├── .dir-locals.el ├── .gitignore ├── .markdown-doctest-setup.js ├── .prettierrc.json ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── docs ├── config.yml └── index.md ├── examples ├── ethSig │ ├── ethSig.rho │ └── rlp.rho └── voting-locker │ ├── assets.js │ ├── chairRole.rho │ ├── locker.rho │ ├── testVoting.js │ ├── voterRole.rho │ └── voting.rho ├── index.js ├── jsconfig.json ├── package.json ├── protobuf ├── CasperMessage.d.ts ├── CasperMessage.js ├── CasperMessage.proto ├── DeployService.js ├── DeployService.proto ├── Either.d.ts ├── Either.js ├── Either.proto ├── Makefile ├── ProposeService.js ├── ProposeService.proto ├── RhoTypes.d.ts ├── RhoTypes.js ├── RhoTypes.proto ├── google │ └── protobuf │ │ ├── any.proto │ │ └── empty.proto ├── interfaces │ ├── CasperMessage.js.flow │ ├── Either.js.flow │ └── RhoTypes.js.flow └── scalapb │ └── scalapb.proto ├── rclient ├── package.json └── src │ ├── asPromise.js │ ├── assets.js │ ├── main.js │ ├── pathlib.js │ ├── rfun.html │ ├── rhoid.js │ ├── secretStorage.js │ ├── sigTool.js │ ├── tool.js │ └── tools.rho ├── src ├── assets.js ├── codec.js ├── curl.js ├── deploySig.js ├── ethProvider.js ├── proxy.js ├── rev-address.js ├── rho-expr.js ├── rhopm.js ├── rnode-openapi-schema.ts └── rnode.js ├── test ├── assets.js ├── liveRNodeTest.js ├── revAddressTest.js ├── target1.rho └── testSigning.js └── yarn.lock /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;; https://emacs.stackexchange.com/a/24188 2 | ((js-mode 3 | ;; TabsAreEvil 4 | (indent-tabs-mode . nil) 5 | ;; match airbnb style (following agoric / erights) 6 | (js2-basic-offset . 2) 7 | (js2-indent-switch-body . true) 8 | )) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules/ 3 | /rclient/keystore.json 4 | /rclient/registry.json 5 | -------------------------------------------------------------------------------- /.markdown-doctest-setup.js: -------------------------------------------------------------------------------- 1 | /* global module, require, Buffer */ 2 | 3 | const assert = require('assert'); 4 | 5 | const rchain = require('.'); 6 | const { grpcMock } = require('./test/testRNode'); 7 | const { RHOCore, RhoTypes, RNode, RholangCrypto, REV, Hex, Ed25519keyPair } = rchain; 8 | 9 | 10 | function config(env, grpcAccess) { 11 | const grpc = env.NODE_ENV === 'production' ? grpcAccess() : grpcMock(); 12 | console.log('doctest env:', env.NODE_ENV || 'dev'); 13 | 14 | return { 15 | babel: false, 16 | require: { 17 | url: require('url'), 18 | grpc: grpc, 19 | 'rchain-api': { RHOCore, RhoTypes, RNode, RholangCrypto, REV, Hex, Ed25519keyPair }, 20 | }, 21 | globals: { 22 | assert, Buffer, 23 | }, 24 | }; 25 | } 26 | 27 | /* global process */ 28 | module.exports = config(process.env, () => require('grpc')); 29 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | cache: 5 | directories: 6 | - "node_modules" 7 | - "rclient/node_modules" 8 | script: 9 | - npm run check 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Code Conventions and Design Notes 2 | 3 | As noted in `.travis.yml`, all contributions must pass `npm run check`, 4 | which runs `test`, `lint`, etc. The `test` check is conventional unit tests. 5 | 6 | Contributions should also pass `npm run integrationTest`, which 7 | requires a running validator node (see below). 8 | 9 | To run both the offline and online tests, use `npm run testAll`. 10 | 11 | 12 | ## Static Typechecking: flow 13 | 14 | We use [flow](https://flow.org/) for static typing. The `npm run 15 | flow-check` script does a complete check and `npm run flow-status` 16 | does an incremental check. 17 | 18 | 19 | 0## RChain Validator Node for Integration testing 20 | 21 | One way to provide a validator node for testing, provided you're OK 22 | with the security risks around `--net host`, is to first have 23 | the node start up and generate some random validator keys: 24 | 25 | ```bash 26 | docker run --rm --net host -v$HOME/.rnode:/var/lib/rnode \ 27 | rchain/rnode run -s 28 | ``` 29 | 30 | Then grab one of the secret keys for use as a validator private key: 31 | 32 | ```bash 33 | first_key=$(cat $(ls ~/.rnode/genesis/*.sk|head -1)) 34 | 35 | docker run --rm --net host -v$HOME/.rnode:/var/lib/rnode \ 36 | rchain/rnode run -s --validator-private-key $first_key 37 | ``` 38 | 39 | 40 | ## Code Style: airbnb 41 | 42 | We follow the [Airbnb JavaScript Style Guide][asg], mostly. Use `npm 43 | run lint`. See `.eslitrc.json` for additional details. 44 | 45 | [asg]: https://github.com/airbnb/javascript#readme 46 | 47 | 48 | ## Object capability (ocap) discipline 49 | 50 | In order to supporting robust composition and cooperation without 51 | vulnerability, code in this project should adhere to [object 52 | capability discipline][ocap]. 53 | 54 | - **Memory safety and encapsulation** 55 | - There is no way to get a reference to an object except by 56 | creating one or being given one at creation or via a message; no 57 | casting integers to pointers, for example. _JavaScript is safe 58 | in this way._ 59 | 60 | From outside an object, there is no way to access the internal 61 | state of the object without the object's consent (where consent 62 | is expressed by responding to messages). _We use `def` (aka 63 | `Object.freeze`) and closures rather than properties on `this` 64 | to achieve this._ 65 | 66 | - **Primitive effects only via references** 67 | - The only way an object can affect the world outside itself is 68 | via references to other objects. All primitives for interacting 69 | with the external world are embodied by primitive objects and 70 | **anything globally accessible is immutable data**. There must be 71 | no `open(filename)` function in the global namespace, nor may 72 | such a function be imported. _It takes some discipline to use 73 | modules in node.js in this way. We use a convention 74 | of only accessing ambient authority inside `if (require.main == 75 | module) { ... }`._ 76 | 77 | [ocap]: http://erights.org/elib/capability/ode/ode-capabilities.html 78 | 79 | 80 | ## Protobuf Encoding: protobuf.js 81 | All of our protobuf encoding and decoding is done using [protobuf.js](https://github.com/dcodeIO/protobuf.js) 82 | 83 | ![protobuf.js diagram](https://camo.githubusercontent.com/f090df881cc6c82ecb7c5d09c9fad550fdfd153e/687474703a2f2f64636f64652e696f2f70726f746f6275662e6a732f746f6f6c7365742e737667) 84 | 85 | 86 | ## Extracting API doc 87 | 88 | We use [documentation.js](https://documentation.js.org/) to build API 89 | docs (docs/index.md) from sources. Use the `docs-watch`, `build:docs`, 90 | or `build:docs-html` npm scripts. 91 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 RChain Cooperative 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RChain-API 2 | 3 | An API for dApp frontends to communicate with the RChain blockchain. 4 | 5 | The [RChain Cooperative][1] is developing a decentralized, economically sustainable public compute infrastructure. Decentralized applications or "dApps" will run their business logic as smart contracts on the blockchain. Their user interfaces will be more traditional programs that interact with the blockchain as a backend. This separation allows dApp developers to create nice abstract interfaces, and allows end users to create their own sovereign interfaces should they choose to do so. 6 | 7 | [1]: https://www.rchain.coop/ 8 | 9 | 10 | ## Quickstart 11 | 12 | Install with `npm install rchain-community/rchain-api`. Then, 13 | to get current block info from testnet: 14 | 15 | 16 | ```js 17 | import { RNode } = from 'rchain-api'; 18 | 19 | const obs = RNode(fetch) 20 | .observer('https://observer.testnet.rchain.coop'); 21 | (async () => { 22 | const blocks = await rnode.getBlocks(1); 23 | assert.ok(blocks[0].blockHash); 24 | })(); 25 | ``` 26 | 27 | If your node is a validator and you have a key to authorize payment, you can deploy code: 28 | 29 | ```js 30 | const { RNode, signDeploy } = require('rchain-api'); 31 | 32 | const val = RNode(fetch) 33 | .validator('https://node1.testnet.rchain-dev.tk'); 34 | 35 | const term = '@"world"!("Hello!")'; 36 | const myKey = '11'.repeat(32); 37 | const timestamp = new Date('2019-04-12T17:59:29.274Z').valueOf(); 38 | const [recent] = await observer.getBlocks(1); 39 | const info = signDeploy(myKey, { timestamp, term, phloLimit: 10000, phloPrice: 1, validAfterBlockNumber: recent.blockNumber }); 40 | rnode.deploy(info).then((message) => { assert(message.startsWith('Success')); }); 41 | ``` 42 | 43 | ## API (OUT OF DATE) 44 | 45 | [./docs/](./docs/index.md) 46 | 47 | 48 | ## Getting access to an RChain node 49 | 50 | Choices include: 51 | 52 | - [RChain testnet][testnet] nodes such as `node4.testnet.rchain-dev.tk` 53 | - a community node at `rnode-test.rhobot.net` 54 | - [running your own RNode][2] 55 | 56 | [testnet]: https://rchain.atlassian.net/wiki/spaces/CORE/pages/678756429/RChain+public+testnet+information 57 | [2]: https://rchain.atlassian.net/wiki/spaces/CORE/pages/428376065/User+guide+for+running+RNode 58 | 59 | 60 | ## Examples and Related Projects 61 | * [Nth Caller](https://github.com/JoshOrndorff/nth-caller-game) a minimal RChain-based dApp game that uses RChain-API 62 | * [Status](https://github.com/JoshOrndorff/RChain-Status) a moderately complex dapp that uses more RChain-API features as well as [RSign](https://github.com/dckc/RSign) 63 | * [Coin Faucet](https://github.com/BlockSpaces/coin-faucet/) An advanced robust dApp that use raspberry PI to airdrop tokens to devices in physical proximity 64 | * [RChain-dbr](https://github.com/dckc/rchain-dbr) A web-of-trust based distributed budgeting and rewards dApp 65 | * [RSign](https://github.com/dckc/RSign) A chrome extension for generating client-side signatures akin to metamask 66 | * [node-client](https://github.com/rchain/rchain/tree/dev/node-client) A similar but less mature RChain API written in python 67 | 68 | 69 | ## License 70 | Copyright 2018-2019 RChain Cooperative 71 | 72 | Apache 2.0 License (See LICENSE.txt) 73 | 74 | Contributions welcome (See CONTRIBUTING.md) 75 | -------------------------------------------------------------------------------- /docs/config.yml: -------------------------------------------------------------------------------- 1 | toc: 2 | - RNode 3 | - RHOCore 4 | - REV 5 | - RholangCrypto 6 | - RegistryProxy 7 | - Ed25519keyPair 8 | - Hex 9 | -------------------------------------------------------------------------------- /examples/ethSig/ethSig.rho: -------------------------------------------------------------------------------- 1 | new trace(`rho:io:stderr`), rl(`rho:registry:lookup`), 2 | verifySignature, sigDER, rlpCh, rlpHash, bufToInt, 3 | EtherWallet, findOrCreateWallet, sendTransaction, 4 | addrToWalletCh, publicToAddr in { 5 | trace!("ethSig") | 6 | 7 | contract EtherWallet(purse, @{addr /\ ByteArray}, return) = { 8 | new self, nonceCh in { 9 | return!(bundle+{*self}) | 10 | nonceCh!(-1) | 11 | 12 | contract self(@"getNonce", return) = { 13 | for(@nonce <- nonceCh) { 14 | nonceCh!(nonce) | return!(nonce) 15 | } 16 | } | 17 | contract self(@"getBalance", return) = { 18 | purse!("getBalance", *return) 19 | } | 20 | contract self(@"deposit", @amount, @src, success) = { 21 | purse!("deposit", amount, src, *success) 22 | } | 23 | 24 | // ISSUE: transfer method to get a purse for use outside this contract? 25 | 26 | contract self(@"sendTransaction", 27 | @{tx /\ ByteArray}, 28 | // ISSUE: the public key can be recovered from the signature, but 29 | // rholang doesn't expose `ecrecover` (yet?) 30 | // so we have the caller supply the pk. 31 | @{senderPk /\ ByteArray }, 32 | eject, return) = { 33 | new senderCh, verifyCh, nonceCh, valueCh, sendIt in { 34 | publicToAddr!(senderPk, *senderCh) | 35 | for(@txSender <- senderCh) { 36 | if(txSender != addr) { 37 | eject!({"message": "senderPk does not match wallet addr", 38 | "expected": addr, "actual": txSender, 39 | "actual from key": senderPk}) 40 | } else { 41 | verifySignature!(tx, senderPk, *verifyCh) | 42 | for(@{"success": false, ...x } <- verifyCh) { eject!({"message": "bad signature"}) } | 43 | for(@{ 44 | "success": true, 45 | "items": [nonceBuf, gasPrice, gasLimit, to, valueBuf, data, v, r, s] 46 | } <- verifyCh) { 47 | // TODO: bufToInteger(value) export from rlp.rho 48 | trace!({"verified sig for value": valueBuf, "to": to}) | 49 | for(@prevNonce <- nonceCh) { 50 | bufToInt!(nonceBuf, *nonceCh) | for (@nonce <- nonceCh) { 51 | if (nonce != (prevNonce + 1)) { 52 | nonceCh!(prevNonce) | 53 | eject!({"message": "bad nonce", 54 | "actual": nonce, "expected": prevNonce + 1}) 55 | } else { 56 | nonceCh!(nonce) | 57 | bufToInt!(valueBuf, *valueCh) | for (@value <- valueCh) { 58 | sendIt!(to, value) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | | 67 | contract sendIt(@{to /\ ByteArray}, @{value /\ Int}) = { 68 | new splitResultCh, toWalletCh, depositSuccessCh in { 69 | findOrCreateWallet!(to, *toWalletCh) | 70 | purse!("split", value, *splitResultCh) | 71 | for(@[payment] <- splitResultCh; toWallet <- toWalletCh) { 72 | toWallet!("deposit", payment, value, *depositSuccessCh) | 73 | for(@true <- depositSuccessCh) { 74 | return!({"sent": value, "to": to}) 75 | } | 76 | for(@false <- depositSuccessCh) { 77 | eject!({"message": "deposit failed (can't happen?)"}) 78 | } 79 | } | 80 | for(@[] <- splitResultCh; _ <- toWalletCh) { 81 | eject!({"message": "Overdraft"}) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | | 90 | contract publicToAddr(@{pubKey /\ ByteArray}, ret) = { 91 | new hashOut in { 92 | @"keccak256Hash"!(pubKey, *hashOut) | 93 | for(@pkHash <- hashOut) { 94 | ret!(pkHash.slice(12, 32)) 95 | } 96 | } 97 | } | 98 | 99 | contract sendTransaction( 100 | @{txBytes /\ ByteArray}, 101 | // ISSUE: the public key can be recovered from the signature, but 102 | // rholang doesn't expose `ecrecover` (yet?) 103 | // so we have the caller supply the pk. 104 | @{senderPk /\ ByteArray }, 105 | eject, 106 | return 107 | ) = { 108 | trace!({"sendTransaction": txBytes.slice(0, 8), "sender": senderPk.slice(0, 8)}) | 109 | new senderAddrCh, senderCh in { 110 | publicToAddr!(senderPk, *senderAddrCh) | 111 | for(@addr <- senderAddrCh) { 112 | findOrCreateWallet!(addr, *senderCh) | 113 | for(senderWallet <- senderCh) { 114 | trace!({"senderWallet": *senderWallet}) | 115 | senderWallet!("sendTransaction", txBytes, senderPk, *eject, *return) 116 | } 117 | } 118 | } 119 | } | 120 | 121 | addrToWalletCh!({}) | 122 | new sysCh, revCh in { 123 | rl!(`rho:id:wdwc36f4ixa6xacck3ddepmgueum7zueuczgthcqp6771kdu8jogm8`, *sysCh) | 124 | for(@(_, sys) <- sysCh) { 125 | @sys!("lookup", "rev", *revCh) | for (rev <- revCh) { 126 | 127 | contract findOrCreateWallet(@{addr /\ ByteArray}, return) = { 128 | trace!({"findOrCreate": addr}) | 129 | for(@addrToWallet <- addrToWalletCh) { 130 | match addrToWallet.get(addr) { 131 | Nil => { 132 | new purseCh, walletCh in { 133 | rev!(*purseCh) | for(purse <- purseCh) { 134 | EtherWallet!(*purse, addr, *walletCh) | 135 | for(@wallet <- walletCh) { 136 | trace!({"add": wallet, "at": addr}) | 137 | addrToWalletCh!(addrToWallet.set(addr, wallet)) | 138 | return!(wallet) 139 | } 140 | } 141 | } 142 | } 143 | wallet => { 144 | trace!({"found": wallet, "at": addr}) | 145 | addrToWalletCh!(addrToWallet) | 146 | return!(wallet) 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } | 154 | 155 | 156 | // ISSUE: how to manage URIs in eval mode? 157 | rl!(`rho:id:yibjsw354knzpsapccnn56397eg5wkpf4azucehw748muawzip4rfh`, *rlpCh) | 158 | 159 | for(@{ 160 | "decode": *rlpDecode, "encode": *rlpEncode, 161 | "oneByte": *oneByte, "decodeVarInt": *decodeVarInt 162 | } <- rlpCh) { 163 | trace!({"rlp": (*rlpEncode, *rlpDecode)}) | 164 | 165 | contract bufToInt(@buf, return) = { decodeVarInt!(buf, *return) } | 166 | 167 | // Verify Signature of rlp-encoded Ethereum Transaction 168 | // ref https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/index.md#verifysignature 169 | contract verifySignature( 170 | @{msg /\ ByteArray}, 171 | // ISSUE: the public key can be recovered from the signature, but 172 | // rholang doesn't expose `ecrecover` (yet?) 173 | // so we have the caller supply the pk. 174 | @{pk /\ ByteArray}, 175 | return 176 | ) = { 177 | // trace!({"verify msg": msg, "pk": pk}) | 178 | new itemsCh, sigCh, hashCh, chainIdCh, verifyCh in { 179 | // ISSUE: handle failure from rlpDecode 180 | rlpDecode!(msg, *trace, *itemsCh) | 181 | for(@[nonce, gasPrice, gasLimit, to, value, data, v, r, s] <- itemsCh) { 182 | // trace!({"decoded tx": [nonce, gasPrice, gasLimit, to, value, data, v, r, s]}) | 183 | 184 | sigDER!(r, s, *sigCh) | 185 | 186 | if (v.nth(0) < 35) { 187 | rlpHash!([nonce, gasPrice, gasLimit, to, value, data], *hashCh) 188 | } else { 189 | // EIP155: chainId = (sigV - 35) // 2; r=0, s=0 190 | oneByte!((v.nth(0) - 35) / 2, *chainIdCh) | 191 | for(@chainId <- chainIdCh) { 192 | rlpHash!([nonce, gasPrice, gasLimit, to, value, data, 193 | // EIP155: chainId = (sigV - 35) // 2; r=0, s=0 194 | chainId, "".hexToBytes(), "".hexToBytes()], *hashCh) 195 | } 196 | } 197 | | 198 | for(@sig <- sigCh; @hash <- hashCh) { 199 | trace!({"verifying sig": sig, "over": hash, "pubKey": pk}) | 200 | @"secp256k1Verify"!(hash, sig, 201 | // ASN1 Distinguished Encoding Rules (DER) for public key 202 | // 4 = OctetString tag 203 | "04".hexToBytes() ++ pk, 204 | *verifyCh) | 205 | for (@ok <- verifyCh) { 206 | // trace!({"sig ok?": ok}) | 207 | if (ok) { 208 | return!({ 209 | "success": true, 210 | "items": [nonce, gasPrice, gasLimit, to, value, data, v, r, s] 211 | }) 212 | } else { 213 | return!({"success": false}) // ISSUE: find out what ethereumjs-tx returns on failure 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } | 220 | contract rlpHash(@item, return) = { 221 | new encodedCh in { 222 | rlpEncode!(item, *encodedCh) | 223 | for(@encoded <- encodedCh) { 224 | @"keccak256Hash"!(encoded, *return) 225 | } 226 | } 227 | } | 228 | 229 | // ASN1 Distinguished Encoding Rules (DER) for signature 230 | // i.e. SEQUENCE { r INTEGER, S INTEGER } 231 | contract sigDER(@{r /\ ByteArray}, @{s /\ ByteArray}, return) = { 232 | // trace!({"sigDERr": r, "s": s}) | 233 | new zpad, rpCh, spCh, lenRSCh, lenRCh, lenSCh in { 234 | zpad!(r, *rpCh) | zpad!(s, *spCh) | 235 | for(@rp <- rpCh; @sp <- spCh) { 236 | // trace!({"rp": rp, "sp": sp}) | 237 | oneByte!(rp.length(), *lenRCh) | 238 | oneByte!(sp.length(), *lenSCh) | 239 | oneByte!(2 + 1 + rp.length() + 1 + sp.length(), *lenRSCh) | 240 | for(@lenR <- lenRCh; @lenS <- lenSCh; @lenRS <- lenRSCh) { 241 | // trace!({"sigDER": (rp, sp), "lenR": lenR, "lenS": lenS, "lenRS": lenRS}) | 242 | return!( 243 | // SEQUENCE 244 | "30".hexToBytes() ++ lenRS ++ 245 | // INTEGER 246 | "02".hexToBytes() ++ lenR ++ rp ++ 247 | "02".hexToBytes() ++ lenS ++ sp 248 | ) 249 | } 250 | } | 251 | contract zpad(@{bn /\ ByteArray}, return) = { 252 | // trace!({"zpad": bn.nth(0)}) | 253 | if (bn.nth(0) >= 128) { 254 | return!("00".hexToBytes() ++ bn) 255 | } else { 256 | return!(bn) 257 | } 258 | } 259 | } 260 | } | 261 | 262 | new resultCh in { 263 | trace!("ethereumjs-tx README test case") | 264 | 265 | verifySignature!( 266 | // tx.serialize().toString('hex') 267 | "f889808609184e72a00082271094000000000000000000000000000000000000000080a47f746573743200000000000000000000000000000000000000000000000000000060005729a0f2d54d3399c9bcd3ac3482a5ffaeddfe68e9a805375f626b4f2f8cf530c2d95aa05b3bb54e6e8db52083a9b674e578c843a87c292f0383ddba168573808d36dc8e".hexToBytes(), 268 | // > tx.getSenderPublicKey().toString('hex') 269 | ("6d9038945ff8f4669201ba1e806c9a46a5034a578e4d52c03152198538039294" ++ 270 | "4efd6c702504d9130573bb939f5c124af95d38168546cc7207a7e0baf14172ff").hexToBytes(), 271 | 272 | // > tx.hash(false).toString('hex') 273 | // or, equivalently: 274 | // const zb = Buffer.from([]) 275 | // > ethUtil.rlphash([tx.nonce, tx.gasPrice, tx.gasLimit, tx.to, tx.value, tx.data, ethUtil.toBuffer(tx._chainId), zb, zb]) 276 | // "df2a7cb6d05278504959987a144c116dbd11cbdc50d6482c5bae84a7f41e2113".hexToBytes(), 277 | *resultCh) | 278 | 279 | for(@actual <- resultCh) { 280 | trace!({"actual from README": actual}) 281 | } 282 | } 283 | | 284 | new resultCh, sendCh, ej in { 285 | verifySignature!( 286 | // https://github.com/ethereumjs/ethereumjs-tx/blob/master/test/txs.json#L22-L32 287 | "f86d068609184e72a0008201f494be862ad9abfe6f22bcb087716c7d89a26051f74c88016345785d8a0000801ca024a484bfa7380860e9fa0a9f5e4b64b985e860ca31abd36e66583f9030c2e29da04d5ef07d9e73fa2fbfdad059591b4f13d0aa79e7634a2bb00174c9200cabb04d".hexToBytes(), 288 | "eca8b8b663b31277cfa6023bc1f4cddd4b5e5f9625c966634b2e499cae52437754536931a0b5dbbb58c62e84440c3c32db4d04a1f4fb1c6ac0bd2d9ad50028a1".hexToBytes(), 289 | // "073c878297a789a52ba1fb9c0c761114e9a0267c0d9ea496aa06fcac5db421e9".hexToBytes(), 290 | *resultCh) | 291 | 292 | for(@actual <- resultCh) { 293 | trace!({"actual test2": actual}) 294 | } | 295 | 296 | sendTransaction!( 297 | "f86d068609184e72a0008201f494be862ad9abfe6f22bcb087716c7d89a26051f74c88016345785d8a0000801ca024a484bfa7380860e9fa0a9f5e4b64b985e860ca31abd36e66583f9030c2e29da04d5ef07d9e73fa2fbfdad059591b4f13d0aa79e7634a2bb00174c9200cabb04d".hexToBytes(), 298 | "eca8b8b663b31277cfa6023bc1f4cddd4b5e5f9625c966634b2e499cae52437754536931a0b5dbbb58c62e84440c3c32db4d04a1f4fb1c6ac0bd2d9ad50028a1".hexToBytes(), 299 | *ej, *sendCh) | 300 | 301 | for(@actual <- sendCh) { 302 | trace!({"actual from sendTransaction test2": actual}) 303 | } | 304 | for(@problem <- ej) { 305 | trace!({"PROBLEM from sendTransaction test2": problem}) 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /examples/ethSig/rlp.rho: -------------------------------------------------------------------------------- 1 | /** rlp.rho - Recursive Length Prefix 2 | * 3 | * "This is a serialisation method for encoding arbitrarily 4 | * structured binary data (byte arrays)." 5 | * 6 | * Appendix B. of the Ethereum yellow paper 7 | * https://ethereum.github.io/yellowpaper/paper.pdf 8 | */ 9 | new encode, decode, oneByte, encodeVarInt, decodeVarInt, 10 | ListOpsCh, test, 11 | trace(`rho:io:stderr`), stdout(`rho:io:stdout`), 12 | insertArbitrary(`rho:registry:insertArbitrary`), lookup(`rho:registry:lookup`) 13 | in { 14 | new uriCh in { 15 | // ISSUE: only register if all tests pass? 16 | insertArbitrary!({ 17 | "decode": bundle+{*decode}, 18 | "encode": bundle+{*encode}, 19 | "encodeVarInt": bundle+{*encodeVarInt}, 20 | "decodeVarInt": bundle+{*decodeVarInt}, 21 | "oneByte": bundle+{*oneByte} 22 | }, *uriCh) | 23 | for (@uri <- uriCh) { 24 | stdout!({"rlp uri": uri}) 25 | } 26 | } | 27 | 28 | 29 | // Examples are taken from https://github.com/ethereum/wiki/wiki/RLP 30 | // ISSUE: move tests to a separate file? 31 | 32 | // • If the byte-array contains solely a single byte and that single 33 | // byte is less than 128, then the input is exactly equal to the 34 | // output. 35 | test!("The encoded integer 0", "00", "00".hexToBytes(), Nil) | 36 | test!("The encoded integer 15", "0f", "0f".hexToBytes(), Nil) | 37 | 38 | // • If the byte-array contains fewer than 56 bytes, then the output 39 | // is equal to the input prefixed by the byte equal to the length 40 | // of the byte array plus 128. 41 | test!("The empty string ('null')", "80", "".hexToBytes(), Nil) | 42 | test!("The integer 0", "80", "".hexToBytes(), Nil) | 43 | test!("The string \"dog\"", "83646f67", "646f67".hexToBytes(), Nil) | 44 | test!("The encoded integer 1024", "820400", "0400".hexToBytes(), Nil) | 45 | 46 | // • Otherwise, the output is equal to the input prefixed by the 47 | // minimal-length byte-array which when interpreted as a 48 | // big-endian integer is equal to the length of the input byte 49 | // array, which is itself prefixed by the number of bytes required 50 | // to faithfully encode this length value plus 183. 51 | test!("The string \"Lorem ipsum dolor sit amet, consectetur adipisicing elit\"", 52 | "b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974", 53 | "4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974".hexToBytes(), Nil) | 54 | 55 | // • If the concatenated serialisations of each contained item is 56 | // less than 56 bytes in length, then the output is equal to that 57 | // concatenation prefixed by the byte equal to the length of this 58 | // byte array plus 192. 59 | test!("The empty list", "c0", [], Nil) | 60 | test!("The list [ \"cat\", \"dog\" ]", "c88363617483646f67", 61 | ["636174".hexToBytes(), "646f67".hexToBytes()], Nil) | 62 | test!("The set theoretical representation of three", 63 | "c7c0c1c0c3c0c1c0", [ [], [[]], [ [], [[]] ] ], Nil) | 64 | 65 | // • Otherwise, the output is equal to the concatenated 66 | // serialisations prefixed by the minimal-length byte-array which 67 | // when interpreted as a big-endian integer is equal to the length 68 | // of the concatenated serialisations byte array, which is itself 69 | // prefixed by the number of bytes required to faithfully encode 70 | // this length value plus 247. 71 | test!("EIP155 test case", 72 | "f864808504a817c800825208943535353535353535353535353535353535353535808025a0044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116da0044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d", 73 | [ 74 | "".hexToBytes(), // nonce 75 | "4a817c800".hexToBytes(), // gasPrice 76 | "5208".hexToBytes(), // gasLimit 77 | "3535353535353535353535353535353535353535".hexToBytes(), // to 78 | "".hexToBytes(), // value 79 | "".hexToBytes(), // data 80 | "25".hexToBytes(), // v 81 | "044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d".hexToBytes(), // r 82 | "044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d".hexToBytes() // s 83 | ], Nil) | 84 | 85 | test!("ill-formed", "", Nil, Nil) | 86 | 87 | trace!("testing") | // Signal that this code is running at all. 88 | 89 | 90 | // Turn Integer into singleton ByteArray. 91 | new hexCh in { 92 | hexCh!({ 93 | 0: "0", 1: "1", 2: "2", 3: "3", 94 | 4: "4", 5: "5", 6: "6", 7: "7", 95 | 8: "8", 9: "9", 10: "a", 11: "b", 96 | 12: "c", 13: "d", 14: "e", 15: "f" 97 | }) | 98 | for(@hexDigit <- hexCh) { 99 | contract oneByte(@{i /\ Int}, return) = { 100 | // trace!({"oneByte": i}) | 101 | return!(( 102 | hexDigit.getOrElse(i / 16, "0") ++ 103 | // ISSUE: coming soon? i % 16 104 | hexDigit.getOrElse(i - (i / 16 * 16), "0") 105 | ).hexToBytes()) 106 | } 107 | } 108 | } | 109 | 110 | // Encode integer as big-endian ByteArray. 111 | // See also toBuffer. 112 | // https://github.com/ethereumjs/ethereumjs-util/blob/master/docs/index.md#tobuffer 113 | contract encodeVarInt(@{x /\ Int}, return) = { 114 | if (x <= 0) { 115 | return!("".hexToBytes()) 116 | } else { 117 | new initCh, tailCh in { 118 | encodeVarInt!(x / 256, *initCh) | 119 | oneByte!(x - ((x / 256) * 256), *tailCh) | 120 | for(@init <- initCh; @tail <- tailCh) { 121 | return!(init ++ tail) 122 | } 123 | } 124 | } 125 | } | 126 | 127 | // Decode bytes [lo .. hi) of input as big-endian integer, 128 | // provided lo >= 0 and hi <= input.length(). 129 | // See also bufferToInt 130 | // https://github.com/ethereumjs/ethereumjs-util/blob/master/docs/index.md#buffertoint 131 | contract decodeVarInt(@{input /\ ByteArray}, @{lo /\ Int}, @{hi /\ Int}, return) = { 132 | // trace!({"toInteger lo": lo, "hi": hi}) | 133 | if (hi <= lo) { 134 | return!(0) 135 | } else if (hi - lo == 1) { 136 | // trace!({"toInteger base": input.nth(lo)}) | 137 | return!(input.nth(lo)) 138 | } else { 139 | new recur in { 140 | decodeVarInt!(lo, hi - 1, *recur) | 141 | for(@bb <- recur) { 142 | // trace!({"toInteger recur": input, "lo": lo, "hi": hi}) | 143 | return!(input.nth(hi - 1) + bb * 256) 144 | } 145 | } 146 | } 147 | } | 148 | 149 | 150 | lookup!(`rho:id:pidw7mxws53r7ubyz3heebf8146ab3z1ct5zq1yw4a9mocg8387aap`, *ListOpsCh) | 151 | for(ListOps <- ListOpsCh) { 152 | trace!({"ListOps": *ListOps}) | 153 | 154 | // ISSUE: pattern doesn't express type: data T = B ByteArray | L [T] 155 | // so encode!([0], *ch) will match the pattern but hang because 0 won't match. 156 | contract encode(@item, return) = { 157 | // trace!({"encode item": item}) | 158 | new shortItem, longItem, concat in { 159 | match item { 160 | ByteArray => { 161 | if (item.length() == 1) { 162 | // rholang `and` does not short-circuit??? 163 | if (item.nth(0) < 128) { return!(item) } 164 | else { longItem!(128, item) } 165 | } else { longItem!(128, item) } 166 | } 167 | [...items] => { 168 | // trace!({"List": item}) | 169 | new payloadCh in { 170 | concat!(item, *payloadCh) | 171 | for(@payload <- payloadCh) { 172 | longItem!(192, payload) 173 | } 174 | } 175 | } 176 | _ => { 177 | Nil // ISSUE: signal error to caller? 178 | } 179 | } | 180 | 181 | contract longItem(@{bias /\ Int}, @{payload /\ ByteArray}) = { 182 | if (payload.length() < 56) { 183 | shortItem!(bias, payload) 184 | } else { 185 | new encodedLengthCh, b0Ch in { 186 | encodeVarInt!(payload.length(), *encodedLengthCh) | 187 | for(@encodedLength <- encodedLengthCh) { 188 | oneByte!(bias + 55 + encodedLength.length(), *b0Ch) | 189 | for(@b0 <- b0Ch) { 190 | return!(b0 ++ encodedLength ++ payload) 191 | } 192 | } 193 | } 194 | } 195 | } | 196 | 197 | contract shortItem(@{bias /\ Int}, @{payload /\ ByteArray}) = { 198 | new lenCh in { 199 | oneByte!(bias + payload.length(), *lenCh) | 200 | for(@len <- lenCh) { 201 | // trace!({"bytes length": len}) | 202 | return!(len ++ payload) 203 | } 204 | } 205 | } | 206 | 207 | contract concat(@items, return) = { 208 | new encodedEachCh, step in { 209 | ListOps!("map", items, *encode, *encodedEachCh) | 210 | for(@encodedEach <- encodedEachCh) { 211 | // trace!({"encodedEach": encodedEach, "item": item}) | 212 | contract step(@tail, @head, return) = { return!(head ++ tail) } | 213 | ListOps!("fold", encodedEach, "".hexToBytes(), *step, *return) 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } | 220 | 221 | // Decode the input or fail and indicate where the problem is. 222 | contract decode(@{input /\ ByteArray}, fail, return) = { 223 | // trace!({"rlp input": input}) | 224 | new decodeItem, outCh in { 225 | decodeItem!(0, input.length(), *outCh) | 226 | for(@item, @next <- outCh) { 227 | // trace!({"item": item, "next": next}) | 228 | if(next == input.length()) { 229 | return!(item) 230 | } else { 231 | fail!(next) 232 | } 233 | } 234 | | 235 | contract decodeItem(@lo, @hi, return) = { 236 | if (lo >= hi) { fail!(lo) } 237 | else { 238 | new stringOrList, returnSlice, 239 | decodeString, shortString, longString, 240 | decodeList, shortList, longList, listItems in { 241 | stringOrList!(input.nth(lo)) | 242 | contract stringOrList(@b0) = { 243 | if (b0 < 128) { returnSlice!(lo, lo + 1) } 244 | else if (b0 < 192) { decodeString!(lo + 1) } 245 | else { decodeList!(lo + 1) } 246 | | 247 | contract returnSlice(@lo, @hi) = { return!(input.slice(lo, hi), hi) } | 248 | contract decodeString(@lo1) = { 249 | // trace!({"decodeString": lo1, "b0": b0}) | 250 | if (b0 < 128 + 56) { shortString!(lo1 + b0 - 128) } 251 | else { longString!(lo1 + b0 + 1 - 128 - 56) } 252 | | 253 | contract shortString(@end) = { 254 | if (end > hi) { fail!(lo) } 255 | else { returnSlice!(lo1, end) } 256 | } 257 | | 258 | contract longString(@lenEnd) = { 259 | if (lenEnd > hi) { fail!(lo1) } 260 | else { 261 | new strLenCh in { 262 | decodeVarInt!(input, lo1, lenEnd, *strLenCh) | for (@strLen <- strLenCh) { 263 | if (lenEnd + strLen > hi) { fail!(lo1) } 264 | else { returnSlice!(lenEnd, lenEnd + strLen) } 265 | } 266 | } 267 | } 268 | } 269 | } 270 | | 271 | contract decodeList(@lo1) = { 272 | if (b0 < 192 + 56) { shortList!(lo1 + b0 - 192) } 273 | else { longList!(lo1 + b0 + 1 - 192 - 56) } 274 | | 275 | contract shortList(@end) = { 276 | if (end > hi) { fail!(lo) } 277 | else { listItems!([], lo1, end) } 278 | } 279 | | 280 | contract longList(@lenEnd) = { 281 | if (lenEnd > hi) { fail!(lo1) } 282 | else { 283 | new listLenCh in { 284 | decodeVarInt!(input, lo1, lenEnd, *listLenCh) | for (@listLen <- listLenCh) { 285 | if (lenEnd + listLen > hi) { fail!(lo1) } 286 | else { listItems!([], lenEnd, lenEnd + listLen) } 287 | } 288 | } 289 | } 290 | } 291 | } 292 | | 293 | contract listItems(@items, @here, @end) = { 294 | // trace!({"list loop": [here, end], "items": items}) | 295 | if (here == end) { return!(items, end) } 296 | else { 297 | new stepCh in { 298 | // trace!({"sub rlp lo": here, "hi": end, 299 | // "input length": input.length(), "items": items}) | 300 | decodeItem!(here, end, *stepCh) | 301 | for(@item, @next <- stepCh) { 302 | listItems!(items ++ [item], next, end) 303 | } 304 | } 305 | } 306 | } 307 | } 308 | } 309 | } 310 | } 311 | } 312 | } 313 | | 314 | contract test(@label, @inputHex, @expected, ack) = { 315 | new errCh, okCh in { 316 | decode!(inputHex.hexToBytes(), *errCh, *okCh) | 317 | for (@ix <- errCh) { 318 | trace!({"test decode": label, "failAt": ix, "input": inputHex, "expected": expected}) 319 | } | 320 | for (@x <- okCh) { 321 | if (x == expected) { 322 | trace!({"pass decode": label}) 323 | } else { 324 | trace!({"FAIL decode": label, "actual": x, "expected": expected}) 325 | } 326 | } 327 | } | 328 | 329 | new encodedCh in { 330 | encode!(expected, *encodedCh) | 331 | for (@actual <- encodedCh) { 332 | if (actual == inputHex.hexToBytes()) { 333 | trace!({"pass encode": label}) 334 | } else { 335 | trace!({"FAIL encode": label, "actual": actual, "expected": inputHex}) 336 | } 337 | } 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /examples/voting-locker/assets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * assets - treat text files as link-time artifacts. 3 | * 4 | * With respect to ocap discipline, we regard this as part of the 5 | * module loading infrastructure rather than a run-time operation. 6 | * 7 | * An alternative implementation would be a pre-processor 8 | * that in-lines the contents of the linked data as constants. 9 | */ 10 | const { readFileSync } = require('fs'); 11 | 12 | exports.link = link; 13 | function link(name) { 14 | return readFileSync(require.resolve(name), 'utf8'); 15 | } 16 | -------------------------------------------------------------------------------- /examples/voting-locker/chairRole.rho: -------------------------------------------------------------------------------- 1 | /** 2 | * chairRole -- coordinate an election 3 | */ 4 | new export(`export:`), chairRole, 5 | trace(`rho:io:stderr`), 6 | lookup(`rho:registry:lookup`), insertArbitrary(`rho:registry:insertArbitrary`) 7 | in { 8 | export!(bundle+{*chairRole}) | 9 | 10 | // Make and register chair locker; return URI. 11 | contract chairRole( 12 | @"makeLocker", @nickname, @pubKey, @choices, 13 | @{@"Ballot"!(BallotURI) | @"Locker"!(LockerURI)}, // JSON object / record 14 | return, 15 | ) = { 16 | new seedEntry, lockerCh, blCh, llCh, ackCh in { 17 | lookup!(BallotURI, *blCh) | 18 | lookup!(LockerURI, *llCh) | 19 | for(Ballot <- blCh; Locker <- llCh) { 20 | Locker!(nickname, pubKey, 21 | 22 | // Note we're _sending_ this ballot seed code, not running it. 23 | for(return <- seedEntry) { 24 | new bCh, self in { 25 | Ballot!( 26 | Set(choices.nth(0), choices.nth(1)), // ISSUE: list -> set 27 | *bCh) | 28 | for(chair, winner <- bCh) { 29 | 30 | // These are 0-ary and total, but they fit in 31 | // the 1-ary partial gateway 32 | contract self(@"giveRightToVote", @voterInboxURI, reject, resolve) = { 33 | new inlCh, ch in { 34 | lookup!(voterInboxURI, *inlCh) | 35 | chair!("giveRightToVote", *ch) | 36 | for(inbox <- inlCh; right <- ch) { 37 | trace!({"inboxURI": voterInboxURI, "inbox": *inbox}) | 38 | inbox!(*right) | 39 | resolve!(Nil) 40 | } 41 | } 42 | } | 43 | 44 | contract self(@"getWinner", _, reject, resolve) = { 45 | // ISSUE: tuples in RHOCore / RSON / JSON 46 | new ch in { 47 | winner!(*ch) | for (@(winner, tally) <- ch) { 48 | resolve!({"winner": winner, "tally": tally}) 49 | } 50 | } 51 | } 52 | } | 53 | return!(bundle+{*self}) 54 | } 55 | }, 56 | *seedEntry, 57 | *lockerCh) 58 | } | 59 | 60 | for (@chairLocker <- lockerCh) { 61 | trace!({"chairLocker created for": nickname}) | 62 | insertArbitrary!(bundle+{chairLocker}, *return) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/voting-locker/locker.rho: -------------------------------------------------------------------------------- 1 | /** 2 | * Locker -- attenuate capabilities with digital signatures. 3 | 4 | Given a "callback function", that is: a process and an entry point 5 | name, we can ask the Locker contract to run the process in an 6 | encapsulated scope and send to the entry point, which returns a seed 7 | capability. The locker gives back an attenuated capability where only 8 | messages signed with the given public key are passed on to the seed. 9 | 10 | contract Locker( 11 | @{label /\ String}, 12 | @{pubKey /\ ByteArray}, 13 | @code, entryPoint, 14 | return) 15 | 16 | The resulting locker can be published in the registry and hence 17 | used in dApps with off-chain components. 18 | 19 | See voterRole.rho and chairRole.rho for demo / test. 20 | 21 | In the test / demo below, for example, we call: 22 | 23 | voterLocker!(sig, 0, 24 | "voteFor", "Rick", *eject, *ack) 25 | 26 | The ack / return channel and the 0 nonce are as usual; the eject 27 | channel allows for partial methods (inspired by the ejector pattern 28 | from E); in particular, signatures may fail to verify. 29 | 30 | Provided the signature verifies, the seed capability is called as: 31 | 32 | seed!("voteFor", "Rick", *eject, *ack) 33 | 34 | TODO: work out how seed, arg are packaged for signature. Currently, 35 | the signature checking algorigthm is a toy: sig == pk ++ pk. 36 | 37 | The demo below uses the Locker for two different roles in a voting 38 | protocol: the chair and a voter. The voter's seed process creates an 39 | inbox channel and shares a write-only bundle of it with the chair via 40 | the registry. The chair sends a right to vote on this channel and the 41 | voter then excercises the vote. 42 | *TODO: more than one voter* 43 | 44 | The demo is also factored for use by off-chain components that pass 45 | plain data in and out (with the exception of return channels). 46 | *TODO: insertArbitrary(*chair, ...)* 47 | 48 | */ 49 | 50 | new export(`export:`), Locker, verify, toyVerify, 51 | trace(`rho:io:stderr`), stdout(`rho:io:stdout`), 52 | insertArbitrary(`rho:registry:insertArbitrary`), lookup(`rho:registry:lookup`) 53 | in { 54 | export!(bundle+{*Locker}) | 55 | 56 | // WARNING: if sig is the wrong size, we get a java.lang.RuntimeException, 57 | // and our nonceCh is stuck. 58 | contract verify( 59 | @{sig /\ ByteArray}, // ed25519 signature of blakeb256Hash of ... TODO 60 | @{pk /\ ByteArray}, // public key 61 | @{nonce /\ Int}, // claimed next nonce 62 | nonceCh, // real nonce storage 63 | @data, // data to be signed (along with nonce) 64 | eject, // failure path (bad nonce or signature) 65 | ack // sig OK path 66 | ) = { 67 | // trace!({"verify sig": sig, "pk": pk, "nonce": nonce}) | 68 | for(@prevNonce <- nonceCh) { 69 | // trace!({"prevNonce": prevNonce}) | 70 | if (nonce == (prevNonce + 1)) { 71 | new result, hashOut in { 72 | // ISSUE: how to package nonce, data? tuples are nice, 73 | // but they don't map easily to JSON. 74 | @"blake2b256Hash"!((nonce, data).toByteArray(), *hashOut) | 75 | for(@hash <- hashOut) { 76 | // trace!({"hash": hash}) | 77 | @"ed25519Verify"!(hash, sig, pk, *result) | for(@r <- result) { 78 | // trace!({"verify": r}) | 79 | if (r) { nonceCh!(nonce) | ack!(Nil) } 80 | else { nonceCh!(prevNonce) | eject!("bad ed25519 signature") } 81 | } 82 | } 83 | } 84 | } else { 85 | // ISSUE: having the locker's nickname in the diagnostic would be nice 86 | nonceCh!(prevNonce) | eject!({"expected nonce": prevNonce + 1}) 87 | } 88 | } 89 | } | 90 | 91 | // toy signature verification: checks that sig == pk ++ pk 92 | contract toyVerify( 93 | @{sig /\ ByteArray}, @{pk /\ ByteArray}, @{nonce /\ Int}, nonceCh, 94 | @data, 95 | eject, ack) = { 96 | // trace!({"verify sig": sig, "pk": pk, "data": data}) | 97 | for(@prevNonce <- nonceCh) { 98 | // trace!({"prevNonce": prevNonce}) | 99 | if (nonce == (prevNonce + 1)) { 100 | if (pk ++ pk == sig) { nonceCh!(nonce) | ack!(Nil) } 101 | else { nonceCh!(prevNonce) | eject!("bad ++ sig") } 102 | } else { 103 | nonceCh!(prevNonce) | eject!({"expected nonce": prevNonce + 1}) 104 | } 105 | } 106 | } | 107 | 108 | contract Locker( 109 | @{label /\ String}, 110 | @{pubKey /\ ByteArray}, 111 | @seedCode, seedEntry, 112 | return) = { 113 | trace!({"Locker": label, "pubKey": pubKey}) | 114 | 115 | new self, seedCh, nonceCh in { 116 | nonceCh!(-1) | 117 | 118 | return!(*self) | 119 | 120 | contract self(@"getNonce", return) = { for(@nonce <- nonceCh) { 121 | nonceCh!(nonce) | // peek 122 | return!(nonce) 123 | } } | 124 | 125 | seedCode | 126 | seedEntry!(*seedCh) | for (seed <- seedCh) { 127 | // trace!({"seed": *seed}) | 128 | 129 | // unary, partial 130 | contract self(@{method /\ String}, 131 | @{sig /\ ByteArray}, @{nonce /\ Int}, 132 | @arg, eject, ret 133 | ) = { 134 | trace!({"Locker": label, "method": method, "param": arg }) | 135 | new okCh in { 136 | // TODO: replace toyVerify by real verify 137 | toyVerify!(sig, pubKey, nonce, *nonceCh, 138 | (method, arg, *eject, *ret), *eject, *okCh) | 139 | for(_ <- okCh) { 140 | // trace!({"sig ok": sig}) | 141 | seed!(method, arg, *eject, *ret) 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /examples/voting-locker/testVoting.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | 3 | const { URL } = require('url'); 4 | 5 | const { RNode, makeProxy, h2b, keyPair } = require('../..'); 6 | 7 | const { link } = require('./assets'); 8 | const { loadRhoModules } = require('../../src/loading'); 9 | 10 | const defaultPayment = { from: '0x1', nonce: 0, phloPrice: 1, phloLimit: 100000 }; 11 | const defaultDeployInfo = { term: '', sig: h2b(''), sigAlgorithm: 'ed25519', timestamp: 0, ...defaultPayment }; 12 | const user = h2b('d72d0a7c0c9378b4874efbf871ae8089dd81f2ed3c54159fffeaba6e6fca4236'); // arbitrary 13 | 14 | const Scenario = { 15 | choices: ['Lincoln', 'Douglas'], // ISSUE: set vs list in JSON / RSON 16 | chair: { 17 | nickname: 'Chip the chair', 18 | keyPair: keyPair(h2b('9217509f61d80a69627daad29796774d1b65d06e70762aa114e9aa534c0d76bb')), 19 | }, 20 | voter: { 21 | nickname: 'Vick the voter', 22 | keyPair: keyPair(h2b('f6664a95992958bbfeb7e6f50bbca2aa7bfd015aec79820caf362a3c874e9247')), 23 | choice: 'Lincoln', 24 | }, 25 | }; 26 | 27 | 28 | async function test({ rnode, clock }) { 29 | const [ 30 | BallotMod, LockerMod, 31 | chairRoleMod, voterRoleMod, 32 | ] = await loadRhoModules([ 33 | link('./voting.rho'), link('./locker.rho'), 34 | link('./chairRole.rho'), link('./voterRole.rho'), 35 | ], user, { rnode, clock }); 36 | 37 | const chairRole = makeProxy(chairRoleMod.URI, { user, ...defaultDeployInfo }, { rnode, clock }); 38 | const voterRole = makeProxy(voterRoleMod.URI, { user, ...defaultDeployInfo }, { rnode, clock }); 39 | 40 | // There's no data dependency between the following two calls, so we should 41 | // be able to do them concurrently, but our proxy mechanism expects replies 42 | // to be available on chain right away and isn't smart enough to wait around 43 | // for the voter to get its right to vote before exercising it. 44 | const chairLockerURI = await chairRole.makeLocker( 45 | Scenario.chair.nickname, h2b(Scenario.chair.keyPair.publicKey()), Scenario.choices, 46 | { Locker: LockerMod.URI, Ballot: BallotMod.URI }, 47 | ); 48 | if (!(chairLockerURI instanceof URL)) { throw new Error('expected URL'); } 49 | const vl /*: mixed */ = await voterRole.makeLocker( 50 | Scenario.voter.nickname, h2b(Scenario.voter.keyPair.publicKey()), LockerMod.URI, 51 | ); 52 | if (!(vl && (typeof vl === 'object') 53 | && ('locker' in vl) && (vl.locker instanceof URL) 54 | && ('inbox' in vl) && (vl.inbox instanceof URL))) { 55 | throw new Error('expected { locker: URI, inbox: URI }'); 56 | } 57 | const { locker, inbox } = vl; 58 | 59 | const chairLocker = makeProxy(chairLockerURI, { user, ...defaultDeployInfo }, { rnode, clock }); 60 | const voterLocker = makeProxy(locker, { user, ...defaultDeployInfo }, { rnode, clock }); 61 | 62 | function toySig(kpr) { 63 | const pkh = kpr.publicKey(); 64 | return h2b(pkh + pkh); 65 | } 66 | 67 | // again, serialization due to proxy return limitations 68 | await chairLocker.giveRightToVote( 69 | toySig(Scenario.chair.keyPair), 0, 70 | inbox, 71 | 'TODO: reject', 72 | ); 73 | await voterLocker.voteFor( 74 | toySig(Scenario.voter.keyPair), 0, 75 | Scenario.voter.choice, 76 | 'TODO: reject', 77 | ); 78 | 79 | const outcome = await chairLocker.getWinner( 80 | toySig(Scenario.chair.keyPair), 1, 81 | 'dummyArg', // ISSUE: more arities in locker 82 | 'TODO: reject', 83 | ); 84 | 85 | console.log(outcome); 86 | } 87 | 88 | 89 | /*global module */ 90 | if (require.main === module) { 91 | /* global process */ 92 | /* eslint-disable global-require */ 93 | const endpoint = { 94 | host: process.env.npm_config_host || 'localhost', 95 | port: parseInt(process.env.npm_config_port || '40401', 10), 96 | }; 97 | const rnode = RNode(require('grpc'), endpoint); 98 | 99 | try { 100 | test({ 101 | rnode, 102 | clock: () => new Date(), 103 | }); 104 | } catch (oops) { 105 | console.error(oops); 106 | process.exit(1); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /examples/voting-locker/voterRole.rho: -------------------------------------------------------------------------------- 1 | /** 2 | * voterRole -- participate in an election 3 | */ 4 | new export(`export:`), voterRole, 5 | trace(`rho:io:stderr`), 6 | lookup(`rho:registry:lookup`), insertArbitrary(`rho:registry:insertArbitrary`) 7 | in { 8 | export!(bundle+{*voterRole}) | 9 | 10 | contract voterRole(@"makeLocker", @nickname, @pubKey, @LockerURI, return) = { 11 | // trace!({"voter MakeLocker": nickname}) | 12 | new seedEntry, llCh, lockerCh, inboxUriCh, ch in { 13 | lookup!(LockerURI, *llCh) | 14 | for(Locker <- llCh) { 15 | Locker!(nickname, pubKey, 16 | 17 | // Note we're _sending_ this voter seed code, not running it. 18 | for(return <- seedEntry) { 19 | new self, rCh, inbox in { 20 | insertArbitrary!(bundle+{*inbox}, *rCh) | 21 | for(@uri <- rCh) { 22 | inboxUriCh!(uri) | 23 | 24 | contract self(@"voteFor", @choice, ej, ack) = { 25 | for(rightToVote <- inbox) { 26 | rightToVote!("vote", choice, *ej, *ack) 27 | } 28 | } 29 | } | 30 | return!(bundle+{*self}) 31 | } 32 | }, 33 | *seedEntry, 34 | *lockerCh 35 | ) 36 | } | 37 | for (voterLocker <- lockerCh) { 38 | insertArbitrary!(bundle+{*voterLocker}, *ch) | 39 | for(@lockerURI <- ch; @inboxURI <- inboxUriCh) { 40 | trace!({"made voter locker": lockerURI, "inbox": inboxURI}) | 41 | return!({"locker": lockerURI, "inbox": inboxURI}) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/voting-locker/voting.rho: -------------------------------------------------------------------------------- 1 | /** 2 | * Ballot -- Voting with delegation 3 | * 4 | * See example / demo below. 5 | * 6 | * Solidity code exerpted from 7 | * https://solidity.readthedocs.io/en/develop/solidity-by-example.html#voting 8 | */ 9 | 10 | 11 | new export(`export:`), Ballot, 12 | trace(`rho:io:stderr`), 13 | stdout(`rho:io:stdout`), 14 | insertArbitrary(`rho:registry:insertArbitrary`) 15 | in { 16 | 17 | export!(bundle+{*Ballot}) | // see loading.js and loader.rho 18 | 19 | /* 20 | contract Ballot { 21 | 22 | address public chairperson; // scoped differently in rholang 23 | 24 | /// Create a new ballot to choose one of `proposalNames`. 25 | constructor(bytes32[] memory proposalNames) public { 26 | ... 27 | */ 28 | // rholang: Set(hd, ...tail) is a pattern for a non-empty set. 29 | contract Ballot(@{proposalNames /\ Set(hd, ...tail)}, return) = { 30 | // chairperson = msg.sender; 31 | // voters[chairperson].weight = 1; // TODO: give chair right to vote 32 | 33 | new chairperson, winningProposalName, voterWeightCh, votedCh, countCh, delegateCh in { 34 | return!(bundle+{*chairperson}, bundle+{*winningProposalName}) | 35 | 36 | /* 37 | // This declares a state variable that 38 | // stores a `Voter` struct for each possible address. 39 | mapping(address => Voter) public voters; 40 | 41 | // This declares a new complex type which will 42 | // be used for variables later. 43 | // It will represent a single voter. 44 | struct Voter { 45 | uint weight; // weight is accumulated by delegation 46 | bool voted; // if true, that person already voted 47 | address delegate; // person delegated to 48 | uint vote; // index of the voted proposal 49 | } 50 | */ 51 | voterWeightCh!({}) | // voterId (aka bundle0{*voter}) -> Int (weight) 52 | votedCh!({}) | // voterId -> proposal 53 | delegateCh!({}) | // voterId -> (delegate) voterId 54 | /* 55 | // This is a type for a single proposal. 56 | struct Proposal { 57 | bytes32 name; // short name (up to 32 bytes) 58 | uint voteCount; // number of accumulated votes 59 | } 60 | 61 | // A dynamically-sized array of `Proposal` structs. 62 | Proposal[] public proposals; 63 | 64 | constructor(...) { 65 | ... // rholang: this is subsumed by use of maps and channels: 66 | 67 | // For each of the provided proposal names, 68 | // create a new proposal object and add it 69 | // to the end of the array. 70 | for (uint i = 0; i < proposalNames.length; i++) { 71 | // `Proposal({...})` creates a temporary 72 | // Proposal object and `proposals.push(...)` 73 | // appends it to the end of `proposals`. 74 | proposals.push(Proposal({ 75 | name: proposalNames[i], 76 | voteCount: 0 77 | })); 78 | } 79 | */ 80 | countCh!({}) | // proposal -> votes / weight 81 | 82 | /* 83 | // Give `voter` the right to vote on this ballot. 84 | // May only be called by `chairperson`. 85 | function giveRightToVote(address voter) public { 86 | ... 87 | */ 88 | contract chairperson(@"giveRightToVote", return) = { 89 | // rholang: check not needed; chairperson is a capability: 90 | // require( 91 | // msg.sender == chairperson, 92 | // "Only chairperson can give right to vote." 93 | // ); 94 | 95 | 96 | // "voter" is perhaps a mis-nomer. "votingRight" might be better. 97 | new voter in { 98 | // rholang: debugging technique: trace inside each for() { ... } 99 | trace!({"chair": *chairperson, "gives right to vote": *voter}) | 100 | return!(bundle+{*voter}) | 101 | 102 | // rholang: check not needed; voter is allocated inside this method: 103 | // require( 104 | // !voters[voter].voted, 105 | // "The voter already voted." 106 | // ); 107 | 108 | // rholang: subsumed by maps, channels 109 | // require(voters[voter].weight == 0); 110 | 111 | // voters[voter].weight = 1; 112 | for (@voterWeight <- voterWeightCh) { 113 | voterWeightCh!(voterWeight.set(bundle0{*voter}, 1)) 114 | }| 115 | 116 | /* 117 | /// Give your vote (including votes delegated to you) 118 | /// to proposal `proposals[proposal].name`. 119 | function vote(uint proposal) public { 120 | Voter storage sender = voters[msg.sender]; 121 | require(sender.weight != 0, "Has no right to vote"); 122 | require(!sender.voted, "Already voted."); 123 | sender.voted = true; 124 | sender.vote = proposal; 125 | 126 | // If `proposal` is out of the range of the array, 127 | // this will throw automatically and revert all 128 | // changes. 129 | proposals[proposal].voteCount += sender.weight; 130 | } 131 | 132 | */ 133 | contract voter(@"vote", @proposal, eject, ack) = { 134 | // trace!({"voter": *voter, "for": proposal}) | 135 | if(proposalNames.contains(proposal) == false) { eject!({"unknown proposal": proposal}) } 136 | else { 137 | for (@votedFor <- votedCh) { 138 | if (votedFor.contains(bundle0{*voter})) { 139 | votedCh!(votedFor) | 140 | eject!("already voted") 141 | } else { 142 | for(@count <- countCh; @voterWeight <- voterWeightCh) { 143 | trace!({"voter": *voter, "weight": voterWeight.get(bundle0{*voter}), 144 | "proposal": proposal}) | 145 | voterWeightCh!(voterWeight) | // peek 146 | votedCh!(votedFor.set(bundle0{*voter}, proposal)) | 147 | countCh!(count.set(proposal, 148 | count.getOrElse(proposal, 0) + 149 | voterWeight.getOrElse(bundle0{*voter}, 0))) | 150 | ack!(Nil) 151 | } 152 | } 153 | } 154 | } 155 | } | 156 | 157 | /* 158 | /// Delegate your vote to the voter `to`. 159 | function delegate(address to) public { 160 | // assigns reference 161 | Voter storage sender = voters[msg.sender]; 162 | require(!sender.voted, "You already voted."); 163 | 164 | require(to != msg.sender, "Self-delegation is disallowed."); 165 | 166 | // Forward the delegation as long as 167 | // `to` also delegated. 168 | // In general, such loops are very dangerous, 169 | // because if they run too long, they might 170 | // need more gas than is available in a block. 171 | // In this case, the delegation will not be executed, 172 | // but in other situations, such loops might 173 | // cause a contract to get "stuck" completely. 174 | while (voters[to].delegate != address(0)) { 175 | to = voters[to].delegate; 176 | 177 | // We found a loop in the delegation, not allowed. 178 | require(to != msg.sender, "Found loop in delegation."); 179 | } 180 | 181 | // Since `sender` is a reference, this 182 | // modifies `voters[msg.sender].voted` 183 | sender.voted = true; 184 | sender.delegate = to; 185 | Voter storage delegate_ = voters[to]; 186 | ... below 187 | } 188 | */ 189 | contract voter(@"delegate", @to, eject, ack) = { 190 | // ISSUE: precedence of {bundle{}} around == 191 | if ({bundle0{to}} == {bundle0{*voter}}) { 192 | eject!("Self-delegation is disallowed.") 193 | } else { 194 | for (@votedFor <- votedCh) { 195 | // trace!({"delegate already voted?": votedFor, "voter": *voter}) | 196 | votedCh!(votedFor) | // ISSUE: peek 197 | 198 | if(votedFor.contains(bundle0{*voter})) { 199 | eject!("You already voted.") 200 | } else { 201 | for (@delegateOf <- delegateCh) { 202 | new loop, finish in { 203 | loop!(bundle0{to}) | 204 | contract loop(@whom) = { 205 | // trace!({"delegate loop": whom}) | 206 | if (whom == {bundle0{*voter}}) { 207 | eject!("Found loop in delegation.") | 208 | delegateCh!(delegateOf) 209 | } else { 210 | if (delegateOf.contains(whom)) { 211 | loop!(delegateOf.get(whom)) 212 | } else { 213 | finish!(whom) 214 | } 215 | } 216 | } | 217 | contract finish(@whom) = { 218 | // trace!({"delegate finish": whom, "voter": *voter}) | 219 | delegateCh!(delegateOf.set(bundle0{*voter}, whom)) | 220 | ack!(Nil) | 221 | 222 | /* 223 | if (delegate_.voted) { 224 | // If the delegate already voted, 225 | // directly add to the number of votes 226 | proposals[delegate_.vote].voteCount += sender.weight; 227 | } else { 228 | // If the delegate did not vote yet, 229 | // add to her weight. 230 | delegate_.weight += sender.weight; 231 | } 232 | */ 233 | for (@count <- countCh; @voterWeight <- voterWeightCh) { 234 | if (votedFor.contains(whom)) { 235 | trace!({"delegate already voted": whom}) | 236 | // If the delegate already voted, 237 | // directly add to the number of votes 238 | countCh!(count.set(votedFor.get(whom), 239 | count.getOrElse(votedFor.get(whom), 0) + 240 | voterWeight.getOrElse(bundle0{*voter}, 1))) | 241 | voterWeightCh!(voterWeight) 242 | } else { 243 | trace!({"add voter weight": voterWeight.get(bundle0{*voter}), 244 | "to delegate weight": voterWeight.get(whom)}) | 245 | // If the delegate did not vote yet, 246 | // add to her weight. 247 | voterWeightCh!(voterWeight.set(whom, 248 | voterWeight.getOrElse(whom, 1) + 249 | voterWeight.getOrElse(bundle0{*voter}, 1))) | 250 | countCh!(count) 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | } 258 | } 259 | } 260 | } 261 | } | 262 | 263 | /* 264 | /// @dev Computes the winning proposal taking all 265 | /// previous votes into account. 266 | function winningProposal() public view 267 | returns (uint winningProposal_) 268 | { 269 | uint winningVoteCount = 0; 270 | for (uint p = 0; p < proposals.length; p++) { 271 | if (proposals[p].voteCount > winningVoteCount) { 272 | winningVoteCount = proposals[p].voteCount; 273 | winningProposal_ = p; 274 | } 275 | } 276 | } 277 | 278 | */ 279 | contract winningProposalName(return) = { 280 | for(@count <- countCh) { 281 | countCh!(count) | // peek 282 | new loop in { 283 | contract loop(@best, @bestCount, @rest) = { 284 | match rest { 285 | Set() => { return!((best, count)) } 286 | Set(hd, ...tail) => { 287 | if (count.getOrElse(hd, 0) > bestCount) { 288 | loop!(hd, count.getOrElse(hd, 0), tail) 289 | } else { 290 | loop!(best, bestCount, tail) 291 | } 292 | } 293 | } 294 | } | 295 | match proposalNames { 296 | Set(hd, ...tail) => { loop!(hd, count.getOrElse(hd, 0), tail) } 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | | 304 | // example / demo 305 | new bCh, v1Ch, v2Ch, v3Ch in { 306 | Ballot!(Set("Lincoln", "Douglas"), *bCh) | 307 | for (chairperson, winningProposalName <- bCh) { 308 | trace!({"Ballot returned": *chairperson}) | 309 | chairperson!("giveRightToVote", *v1Ch) | 310 | chairperson!("giveRightToVote", *v2Ch) | 311 | chairperson!("giveRightToVote", *v3Ch) | 312 | for(v1 <- v1Ch; v2 <- v2Ch; v3 <- v3Ch) { 313 | v3!("delegate", bundle0{*v2}, *trace, *v3Ch) | 314 | v3!("delegate", bundle0{*v3}, *trace, *v3Ch) | 315 | for(_ <- v3Ch) { 316 | v1!("vote", "Douglas", *trace, *v1Ch) | 317 | v1!("vote", "Lincoln", *trace, *v1Ch) | 318 | v1!("vote", "Abe Lincoln", *trace, *v1Ch) | 319 | v2!("vote", "Lincoln", *trace, *v2Ch) | 320 | for(_ <- v1Ch; _ <- v2Ch) { 321 | // trace!("votes done") | 322 | winningProposalName!(*bCh) | 323 | for(@(winner, tally) <- bCh) { 324 | trace!({"winner": winner, "tally": tally}) 325 | } 326 | } 327 | } 328 | } 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // TODO: doctests? 4 | 5 | /** 6 | * @typedef { import('./src/rnode').Observer } Observer 7 | * @typedef { import('./src/rnode').Validator } Validator 8 | * @typedef { import('./src/proxy').Account } Account 9 | * @typedef { import('./src/rev-address').RevAccount } RevAccount 10 | * @typedef { import('./src/rnode-openapi-schema').ExploratoryDeployResponse } ExploratoryDeployResponse 11 | * @typedef { import('./src/rnode-openapi-schema').DeployData } DeployData 12 | * @typedef { import('./src/rnode-openapi-schema').DeployRequest } DeployRequest 13 | * @typedef { import('./src/rnode-openapi-schema').DeployInfo } DeployInfo 14 | */ 15 | 16 | import * as rhopmAll from './src/rhopm'; 17 | 18 | export { RNode } from './src/rnode'; 19 | export { RhoExpr } from './src/rho-expr'; 20 | export { Base16, Base58 } from './src/codec'; 21 | export { sign as signDeploy, signMetaMask } from './src/deploySig'; 22 | export { 23 | getAddrFromEth, 24 | getAddrFromPublicKey, 25 | getAddrFromPrivateKey, 26 | verifyRevAddr, 27 | createRevAccount, 28 | } from './src/rev-address'; 29 | export { 30 | makeAccount, 31 | startTerm, 32 | listenAtDeployId, 33 | makeConnection, 34 | } from './src/proxy'; 35 | export { getEthProvider, MetaMaskAccount } from './src/ethProvider'; 36 | export const rhopm = rhopmAll; 37 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | // This file can contain .js-specific Typescript compiler config. 2 | { 3 | "compilerOptions": { 4 | "target": "es2020", 5 | 6 | "noEmit": false, 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "outDir": "types", 10 | 11 | "strictNullChecks": true, 12 | "moduleResolution": "node" 13 | }, 14 | "include": ["exports.js", "index.js", "src/**.js"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rchain-api", 3 | "version": "0.10.0", 4 | "description": "RChain client for node.js, browsers", 5 | "main": "index.js", 6 | "types": "types/index.d.ts", 7 | "scripts": { 8 | "check": "yarn test && yarn lint", 9 | "test": "ava --verbose", 10 | "test:md": "markdown-doctest", 11 | "lint-fix": "yarn lint --fix", 12 | "lint-check": "yarn lint", 13 | "lint": "yarn lint:types && yarn lint:eslint", 14 | "lint:eslint": "eslint", 15 | "lint:types": "tsc -p jsconfig.json", 16 | "build:docs": "documentation build -f md --github --sort-order alpha index.js src/**.js --shallow -o docs/index.md && markdown-doctest", 17 | "build:docs-html": "documentation build -f html --github --sort-order alpha index.js src/**.js --shallow -o docs/", 18 | "doc-watch": "documentation serve --watch index.js --config docs/config.yml src/**.js --shallow" 19 | }, 20 | "ava": { 21 | "files": [ 22 | "test/**/test*.js" 23 | ], 24 | "require": [ 25 | "esm" 26 | ], 27 | "timeout": "2m" 28 | }, 29 | "config": { 30 | "host": "localhost", 31 | "port": "40401" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/rchain-community/RChain-API.git" 36 | }, 37 | "keywords": [ 38 | "rchain", 39 | "blockchain", 40 | "rholang" 41 | ], 42 | "contributors": [ 43 | "Dan Connolly (http://www.madmode.com)", 44 | "Joshy Orndorff", 45 | "Chris Williams" 46 | ], 47 | "license": "Apache-2.0", 48 | "bugs": { 49 | "url": "https://github.com/rchain-community/RChain-API/issues" 50 | }, 51 | "homepage": "https://github.com/rchain-community/RChain-API#readme", 52 | "dependencies": { 53 | "@jessie.js/eslint-plugin": "^0.1.3", 54 | "blakejs": "^1.1.0", 55 | "bs58": "^4.0.1", 56 | "elliptic": "^6.5.4", 57 | "ethereumjs-util": "^7.0.7", 58 | "google-protobuf": "^3.13.0", 59 | "jessie.js": "^0.2.0", 60 | "js-sha3": "^0.8.0" 61 | }, 62 | "devDependencies": { 63 | "@types/node": "^14.11.2", 64 | "ava": "^3.13.0", 65 | "eslint": "^8.6.0", 66 | "eslint-config-airbnb": "^18.2.0", 67 | "eslint-config-airbnb-base": "^14.2.0", 68 | "eslint-config-prettier": "^6.11.0", 69 | "eslint-plugin-import": "^2.22.0", 70 | "eslint-plugin-prettier": "^3.1.4", 71 | "esm": "^3.2.25", 72 | "prettier": "^2.1.1", 73 | "typescript": "^4.0.2" 74 | }, 75 | "eslintConfig": { 76 | "parserOptions": { 77 | "sourceType": "module", 78 | "ecmaVersion": 6 79 | }, 80 | "extends": [ 81 | "plugin:@jessie.js/recommended" 82 | ] 83 | }, 84 | "TODO": { 85 | "documentation": "^8.0.2" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /protobuf/CasperMessage.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * The main API is `DeployService`. 3 | */ 4 | syntax = "proto3"; 5 | package coop.rchain.casper.protocol; 6 | 7 | import "google/protobuf/empty.proto"; 8 | 9 | // If you are building for other languages "scalapb.proto" 10 | // can be manually obtained here: 11 | // https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto 12 | // make a scalapb directory in this file's location and place it inside 13 | 14 | import "scalapb/scalapb.proto"; 15 | import "RhoTypes.proto"; 16 | import "Either.proto"; 17 | 18 | option (scalapb.options) = { 19 | package_name: "coop.rchain.casper.protocol" 20 | flat_package: true 21 | single_file: true 22 | preamble: "sealed trait CasperMessage" 23 | }; 24 | 25 | message HasBlockRequest { 26 | option (scalapb.message).extends = "CasperMessage"; 27 | bytes hash = 1; 28 | } 29 | 30 | message HasBlock { 31 | option (scalapb.message).extends = "CasperMessage"; 32 | bytes hash = 1; 33 | } 34 | 35 | message BlockRequest { 36 | option (scalapb.message).extends = "CasperMessage"; 37 | bytes hash = 1; 38 | } 39 | 40 | message ForkChoiceTipRequest { 41 | option (scalapb.message).extends = "CasperMessage"; 42 | } 43 | 44 | // ---------- Signing Protocol --------- 45 | message ApprovedBlockCandidate { 46 | BlockMessage block = 1; 47 | int32 requiredSigs = 2; 48 | } 49 | 50 | message UnapprovedBlock { 51 | option (scalapb.message).extends = "CasperMessage"; 52 | ApprovedBlockCandidate candidate = 1; 53 | int64 timestamp = 2; 54 | int64 duration = 3; 55 | } 56 | 57 | message Signature { 58 | bytes publicKey = 1; 59 | string algorithm = 2; 60 | bytes sig = 3; 61 | } 62 | 63 | message BlockApproval { 64 | option (scalapb.message).extends = "CasperMessage"; 65 | ApprovedBlockCandidate candidate = 1; 66 | Signature sig = 2; 67 | } 68 | 69 | message ApprovedBlock { 70 | option (scalapb.message).extends = "CasperMessage"; 71 | ApprovedBlockCandidate candidate = 1; 72 | repeated Signature sigs = 2; 73 | } 74 | 75 | message ApprovedBlockRequest { 76 | option (scalapb.message).extends = "CasperMessage"; 77 | string identifier = 1; 78 | } 79 | 80 | message NoApprovedBlockAvailable { 81 | option (scalapb.message).extends = "CasperMessage"; 82 | string identifier = 1; 83 | string nodeIdentifer = 2; 84 | } 85 | 86 | // ------- End Signing Protocol -------- 87 | 88 | // --------- Core Protocol -------- 89 | message BlockMessage { 90 | option (scalapb.message).extends = "CasperMessage"; 91 | bytes blockHash = 1; // obtained by hashing the information in the header 92 | Header header = 2; 93 | Body body = 3; 94 | repeated Justification justifications = 4; // map of all validators to latest blocks based on current view 95 | bytes sender = 5; // public key of the validator that created the block 96 | int32 seqNum = 6; // number of blocks created by the validator 97 | bytes sig = 7; // signature generated by signing `hash(hash(justification) concat blockHash)`. 98 | string sigAlgorithm = 8; // name of the algorithm used to sign 99 | string shardId = 9; // identifier of the shard where the block was created 100 | bytes extraBytes = 10; 101 | } 102 | 103 | message BlockMetadataInternal { 104 | // This message in mapped to a different Scala class because of protobuf's inability to create map for 105 | // bonds. 106 | option (scalapb.message).type = "coop.rchain.models.BlockMetadata"; 107 | 108 | bytes blockHash = 1; 109 | repeated bytes parents = 2 [(scalapb.field).collection_type="collection.immutable.List"]; 110 | bytes sender = 3; 111 | repeated Justification justifications = 4 [(scalapb.field).collection_type="collection.immutable.List"]; 112 | repeated Bond bonds = 5 [(scalapb.field).collection_type="collection.immutable.List"]; 113 | int64 blockNum = 6; 114 | int32 seqNum = 7; 115 | bool invalid = 8; // whether the block was marked as invalid 116 | } 117 | 118 | message Header { 119 | repeated bytes parentsHashList = 1; //list of parent block hashes 120 | bytes postStateHash = 2; 121 | bytes deploysHash = 3; 122 | int64 timestamp = 5; 123 | int64 version = 6; 124 | int32 deployCount = 7; 125 | bytes extraBytes = 8; 126 | } 127 | 128 | /** 129 | * Note: deploys are uniquely keyed by `user`, `timestamp`. 130 | * 131 | * **TODO**: details of signatures and payment. See RHOL-781 132 | */ 133 | message DeployData { 134 | bytes deployer = 1; //public key 135 | string term = 2; //rholang source code to deploy (will be parsed into `Par`) 136 | int64 timestamp = 3; //millisecond timestamp 137 | bytes sig = 4; //signature of (hash(term) + timestamp) using private key 138 | string sigAlgorithm = 5; //name of the algorithm used to sign 139 | int64 phloPrice = 7; //phlo price 140 | int64 phloLimit = 8; //phlo limit for the deployment 141 | int64 validAfterBlockNumber = 10; 142 | } 143 | 144 | message ProcessedDeploy { 145 | DeployData deploy = 1; 146 | PCost cost = 2 ; 147 | repeated Event deployLog = 3; //the new terms and comm. rule reductions from this deploy 148 | repeated Event paymentLog = 4; //the comm. rule reductions from the payment for the deploy 149 | bool errored = 5; //true if deploy encountered a user error 150 | } 151 | 152 | message Body { 153 | RChainState state = 1; 154 | repeated ProcessedDeploy deploys = 2; 155 | bytes extraBytes = 3; 156 | } 157 | 158 | message Justification { 159 | bytes validator = 1; 160 | bytes latestBlockHash = 2; 161 | } 162 | 163 | message RChainState { 164 | bytes preStateHash = 1; //hash of the tuplespace contents before new deploys 165 | bytes postStateHash = 2; //hash of the tuplespace contents after new deploys 166 | 167 | //Internals of what will be the "blessed" PoS contract 168 | //(which will be part of the tuplespace in the real implementation). 169 | repeated Bond bonds = 3; 170 | int64 blockNumber = 4; 171 | } 172 | 173 | message Event { 174 | oneof event_instance { 175 | ProduceEvent produce = 1; 176 | ConsumeEvent consume = 2; 177 | CommEvent comm = 3; 178 | } 179 | } 180 | 181 | message ProduceEvent { 182 | bytes channelsHash = 1; 183 | bytes hash = 2; 184 | bool persistent = 3; 185 | int32 sequenceNumber = 4; 186 | } 187 | 188 | message ConsumeEvent { 189 | repeated bytes channelsHashes = 1; 190 | bytes hash = 2; 191 | bool persistent = 3; 192 | int32 sequenceNumber = 4; 193 | } 194 | 195 | message CommEvent { 196 | ConsumeEvent consume = 1; 197 | repeated ProduceEvent produces = 2; 198 | } 199 | 200 | message Bond { 201 | bytes validator = 1; 202 | int64 stake = 2; 203 | } 204 | // --------- End Core Protocol -------- 205 | -------------------------------------------------------------------------------- /protobuf/DeployService.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * The main API is `DeployService`. 3 | */ 4 | syntax = "proto3"; 5 | package coop.rchain.casper.protocol; 6 | 7 | import "CasperMessage.proto"; 8 | import "google/protobuf/empty.proto"; 9 | 10 | // If you are building for other languages "scalapb.proto" 11 | // can be manually obtained here: 12 | // https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto 13 | // make a scalapb directory in this file's location and place it inside 14 | 15 | import "scalapb/scalapb.proto"; 16 | import "RhoTypes.proto"; 17 | import "Either.proto"; 18 | 19 | option (scalapb.options) = { 20 | package_name: "coop.rchain.casper.protocol" 21 | flat_package: true 22 | single_file: true 23 | }; 24 | 25 | // Use `DoDeploy` to queue deployments of Rholang code and then 26 | // `createBlock` to make a new block with the results of running them 27 | // all. 28 | // 29 | // To get results back, use `listenForDataAtName`. 30 | service DeployService { 31 | // Queue deployment of Rholang code (or fail to parse). 32 | // Returns on success DeployServiceResponse 33 | rpc DoDeploy(DeployData) returns (Either) {} 34 | // Get details about a particular block. 35 | // Returns on success BlockQueryResponse 36 | rpc getBlock(BlockQuery) returns (Either) {} 37 | // Get dag 38 | // Returns on success VisualizeBlocksResponse 39 | rpc visualizeDag(VisualizeDagQuery) returns (stream Either) {} 40 | rpc machineVerifiableDag(MachineVerifyQuery) returns (Either) {} 41 | // Returns on success LightBlockInfo 42 | rpc showMainChain(BlocksQuery) returns (stream Either) {} 43 | // Get a summary of blocks on the blockchain. 44 | // Returns on success LightBlockInfo 45 | rpc getBlocks(BlocksQuery) returns (stream Either) {} 46 | // Find data sent to a name. 47 | // Returns on success ListeningNameDataResponse 48 | rpc listenForDataAtName(DataAtNameQuery) returns (Either) {} 49 | // Find processes receiving on a name. 50 | // Returns on success ListeningNameContinuationResponse 51 | rpc listenForContinuationAtName(ContinuationAtNameQuery) returns (Either) {} 52 | // Find block from a deploy. 53 | // Returns on success BlockQueryResponse 54 | rpc findBlockWithDeploy(FindDeployInBlockQuery) returns (Either) {} 55 | // Find block containing a deploy. 56 | // Returns on success LightBlockQueryResponse 57 | rpc findDeploy(FindDeployQuery) returns (Either) {} 58 | // Preview new top-level unforgeable names (for example, to compute signatures over them). 59 | // Returns on success PrivateNamePreviewResponse 60 | rpc previewPrivateNames(PrivateNamePreviewQuery) returns (Either) {} 61 | // Get details about a particular block. 62 | // Returns on success BlockQueryResponse 63 | rpc lastFinalizedBlock(LastFinalizedBlockQuery) returns (Either) {} 64 | } 65 | 66 | message FindDeployQuery { 67 | bytes deployId = 1; 68 | } 69 | 70 | message FindDeployInBlockQuery { 71 | bytes user = 1; 72 | int64 timestamp = 2; 73 | } 74 | 75 | message BlockQuery { 76 | string hash = 1; 77 | } 78 | 79 | message BlocksQuery { 80 | int32 depth = 1; 81 | } 82 | 83 | message DataAtNameQuery { 84 | int32 depth = 1; 85 | Par name = 2; 86 | } 87 | 88 | message ContinuationAtNameQuery { 89 | int32 depth = 1; 90 | repeated Par names = 2; 91 | } 92 | 93 | message DeployServiceResponse { 94 | string message = 1; 95 | } 96 | 97 | message BlockQueryResponse { 98 | BlockInfo blockInfo = 1; 99 | } 100 | 101 | message LightBlockQueryResponse { 102 | LightBlockInfo blockInfo = 1; 103 | } 104 | 105 | message VisualizeDagQuery { 106 | int32 depth = 1; 107 | bool showJustificationLines = 2; 108 | } 109 | 110 | message VisualizeBlocksResponse { 111 | string content = 1; 112 | } 113 | 114 | message MachineVerifyQuery { 115 | 116 | } 117 | 118 | message MachineVerifyResponse { 119 | string content = 1; 120 | } 121 | 122 | message ListeningNameDataResponse { 123 | repeated DataWithBlockInfo blockResults = 1; 124 | int32 length = 2; 125 | } 126 | 127 | message ListeningNameContinuationResponse { 128 | repeated ContinuationsWithBlockInfo blockResults = 1; 129 | int32 length = 2; 130 | } 131 | 132 | message PrivateNamePreviewQuery { 133 | bytes user = 1; // public key a la DeployData 134 | int64 timestamp = 2; // millisecond timestamp 135 | int32 nameQty = 3; // how many names to preview? (max: 1024) 136 | } 137 | 138 | message PrivateNamePreviewResponse { 139 | repeated bytes ids = 1; // a la GPrivate 140 | } 141 | 142 | message LastFinalizedBlockQuery { 143 | 144 | } 145 | 146 | message LastFinalizedBlockResponse { 147 | BlockInfo blockInfo = 1; 148 | } 149 | 150 | message LightBlockInfo { 151 | string blockHash = 1; 152 | string blockSize = 2; 153 | int64 blockNumber = 3; 154 | int64 version = 4; 155 | int32 deployCount = 5; 156 | string tupleSpaceHash = 6; // Same as postStateHash of BlockMessage 157 | int64 timestamp = 7; 158 | float faultTolerance = 8; 159 | string mainParentHash = 9; 160 | repeated string parentsHashList = 10; 161 | string sender = 11; 162 | } 163 | 164 | // For node clients, see BlockMessage for actual Casper protocol Block representation 165 | message BlockInfo { 166 | string blockHash = 1; 167 | string blockSize = 2; 168 | int64 blockNumber = 3; 169 | int64 version = 4; 170 | int32 deployCount = 5; 171 | string tupleSpaceHash = 6; // Same as postStateHash of BlockMessage 172 | int64 timestamp = 7; 173 | float faultTolerance = 8; 174 | string mainParentHash = 9; 175 | repeated string parentsHashList = 10; 176 | string sender = 11; 177 | string shardId = 12; 178 | repeated string bondsValidatorList = 13; 179 | repeated string deployCost = 14; 180 | } 181 | 182 | message DataWithBlockInfo { 183 | repeated Par postBlockData = 1; 184 | LightBlockInfo block = 2; 185 | } 186 | 187 | message ContinuationsWithBlockInfo { 188 | repeated WaitingContinuationInfo postBlockContinuations = 1; 189 | LightBlockInfo block = 2; 190 | } 191 | 192 | message WaitingContinuationInfo { 193 | repeated BindPattern postBlockPatterns = 1; 194 | Par postBlockContinuation = 2; 195 | } 196 | -------------------------------------------------------------------------------- /protobuf/Either.d.ts: -------------------------------------------------------------------------------- 1 | import * as $protobuf from "protobufjs"; 2 | export interface IEitherAny { 3 | type_url?: (string|null); 4 | value?: (Uint8Array|null); 5 | } 6 | 7 | export class EitherAny implements IEitherAny { 8 | constructor(properties?: IEitherAny); 9 | public type_url: string; 10 | public value: Uint8Array; 11 | public static create(properties?: IEitherAny): EitherAny; 12 | public static encode(message: IEitherAny, writer?: $protobuf.Writer): $protobuf.Writer; 13 | public static encodeDelimited(message: IEitherAny, writer?: $protobuf.Writer): $protobuf.Writer; 14 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): EitherAny; 15 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): EitherAny; 16 | public static verify(message: { [k: string]: any }): (string|null); 17 | public static fromObject(object: { [k: string]: any }): EitherAny; 18 | public static toObject(message: EitherAny, options?: $protobuf.IConversionOptions): { [k: string]: any }; 19 | public toJSON(): { [k: string]: any }; 20 | } 21 | 22 | export interface IEitherError { 23 | messages?: (string[]|null); 24 | } 25 | 26 | export class EitherError implements IEitherError { 27 | constructor(properties?: IEitherError); 28 | public messages: string[]; 29 | public static create(properties?: IEitherError): EitherError; 30 | public static encode(message: IEitherError, writer?: $protobuf.Writer): $protobuf.Writer; 31 | public static encodeDelimited(message: IEitherError, writer?: $protobuf.Writer): $protobuf.Writer; 32 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): EitherError; 33 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): EitherError; 34 | public static verify(message: { [k: string]: any }): (string|null); 35 | public static fromObject(object: { [k: string]: any }): EitherError; 36 | public static toObject(message: EitherError, options?: $protobuf.IConversionOptions): { [k: string]: any }; 37 | public toJSON(): { [k: string]: any }; 38 | } 39 | 40 | export interface IEitherSuccess { 41 | response?: (IEitherAny|null); 42 | } 43 | 44 | export class EitherSuccess implements IEitherSuccess { 45 | constructor(properties?: IEitherSuccess); 46 | public response?: (IEitherAny|null); 47 | public static create(properties?: IEitherSuccess): EitherSuccess; 48 | public static encode(message: IEitherSuccess, writer?: $protobuf.Writer): $protobuf.Writer; 49 | public static encodeDelimited(message: IEitherSuccess, writer?: $protobuf.Writer): $protobuf.Writer; 50 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): EitherSuccess; 51 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): EitherSuccess; 52 | public static verify(message: { [k: string]: any }): (string|null); 53 | public static fromObject(object: { [k: string]: any }): EitherSuccess; 54 | public static toObject(message: EitherSuccess, options?: $protobuf.IConversionOptions): { [k: string]: any }; 55 | public toJSON(): { [k: string]: any }; 56 | } 57 | 58 | export interface IEither { 59 | error?: (IEitherError|null); 60 | success?: (IEitherSuccess|null); 61 | } 62 | 63 | export class Either implements IEither { 64 | constructor(properties?: IEither); 65 | public error?: (IEitherError|null); 66 | public success?: (IEitherSuccess|null); 67 | public content?: ("error"|"success"); 68 | public static create(properties?: IEither): Either; 69 | public static encode(message: IEither, writer?: $protobuf.Writer): $protobuf.Writer; 70 | public static encodeDelimited(message: IEither, writer?: $protobuf.Writer): $protobuf.Writer; 71 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): Either; 72 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): Either; 73 | public static verify(message: { [k: string]: any }): (string|null); 74 | public static fromObject(object: { [k: string]: any }): Either; 75 | public static toObject(message: Either, options?: $protobuf.IConversionOptions): { [k: string]: any }; 76 | public toJSON(): { [k: string]: any }; 77 | } 78 | 79 | export namespace google { 80 | 81 | namespace protobuf { 82 | 83 | interface IAny { 84 | type_url?: (string|null); 85 | value?: (Uint8Array|null); 86 | } 87 | 88 | class Any implements IAny { 89 | constructor(properties?: google.protobuf.IAny); 90 | public type_url: string; 91 | public value: Uint8Array; 92 | public static create(properties?: google.protobuf.IAny): google.protobuf.Any; 93 | public static encode(message: google.protobuf.IAny, writer?: $protobuf.Writer): $protobuf.Writer; 94 | public static encodeDelimited(message: google.protobuf.IAny, writer?: $protobuf.Writer): $protobuf.Writer; 95 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): google.protobuf.Any; 96 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): google.protobuf.Any; 97 | public static verify(message: { [k: string]: any }): (string|null); 98 | public static fromObject(object: { [k: string]: any }): google.protobuf.Any; 99 | public static toObject(message: google.protobuf.Any, options?: $protobuf.IConversionOptions): { [k: string]: any }; 100 | public toJSON(): { [k: string]: any }; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /protobuf/Either.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "scalapb/scalapb.proto"; 4 | import "google/protobuf/any.proto"; 5 | 6 | option java_package = "coop.rchain.either"; 7 | 8 | message EitherAny { 9 | string type_url = 1; 10 | bytes value = 2; 11 | } 12 | 13 | message EitherError { 14 | repeated string messages = 1; 15 | } 16 | 17 | message EitherSuccess { 18 | EitherAny response = 1; 19 | } 20 | 21 | message Either { 22 | oneof content { 23 | EitherError error = 1; 24 | EitherSuccess success = 2; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /protobuf/Makefile: -------------------------------------------------------------------------------- 1 | # Automatically fetch .proto files 2 | 3 | WGET=wget 4 | PERL=perl 5 | PBJS=../node_modules/.bin/pbjs 6 | PBTS=../node_modules/.bin/pbts 7 | 8 | # npm install -g flowgen 9 | FLOWGEN=flowgen 10 | 11 | REV=v0.9.12 12 | # REV=master 13 | # REV=dev 14 | 15 | GOOG=google/protobuf/empty.proto google/protobuf/any.proto 16 | PROTO_SRC=DeployService.proto ProposeService.proto \ 17 | CasperMessage.proto RhoTypes.proto Either.proto \ 18 | $(GOOG) 19 | 20 | STATIC_JS=DeployService.js ProposeService.js RhoTypes.js 21 | STATIC_TS=DeployService.d.ts ProposeService.d.ts CasperMessage.d.ts RhoTypes.d.ts Either.d.ts 22 | STATIC_FLOW=interfaces/DeployService.js.flow interfaces/ProposeService.js.flow \ 23 | interfaces/CasperMessage.js.flow interfaces/RhoTypes.js.flow interfaces/Either.js.flow \ 24 | interfaces/protobufjs.js.flow 25 | 26 | download: $(PROTO_SRC) 27 | 28 | protoclean: 29 | rm -rf $(PROTO_SRC) 30 | 31 | realclean: 32 | rm -rf $(PROTO_SRC) $(STATIC_JS) $(STATIC_TS) $(STATIC_FLOW) 33 | 34 | RAW_GH=https://raw.githubusercontent.com 35 | R_SRC=$(RAW_GH)/rchain/rchain/$(REV) 36 | 37 | DeployService.proto: 38 | $(WGET) -O $@ $(R_SRC)/models/src/main/protobuf/DeployService.proto 39 | 40 | ProposeService.proto: 41 | $(WGET) -O $@ $(R_SRC)/models/src/main/protobuf/ProposeService.proto 42 | 43 | CasperMessage.proto: 44 | $(WGET) -O $@ $(R_SRC)/models/src/main/protobuf/CasperMessage.proto 45 | 46 | RhoTypes.proto: 47 | $(WGET) -O $@ $(R_SRC)/models/src/main/protobuf/RhoTypes.proto 48 | 49 | Either.proto: 50 | $(WGET) -O $@ $(R_SRC)/models/src/main/protobuf/Either.proto 51 | 52 | 53 | google/protobuf/empty.proto: 54 | mkdir -p google/protobuf 55 | $(WGET) -O $@ $(RAW_GH)/google/protobuf/v3.5.1/src/google/protobuf/empty.proto 56 | 57 | google/protobuf/any.proto: 58 | mkdir -p google/protobuf 59 | $(WGET) -O $@ $(RAW_GH)/google/protobuf/v3.5.1/src/google/protobuf/any.proto 60 | 61 | ## static codegen (WIP) 62 | static: $(STATIC_JS) 63 | 64 | DeployService.js: DeployService.proto CasperMessage.proto Either.proto RhoTypes.proto 65 | $(PBJS) -t static-module -w commonjs --no-comments --keep-case -o $@ \ 66 | DeployService.proto CasperMessage.proto Either.proto RhoTypes.proto 67 | 68 | ProposeService.js: ProposeService.proto CasperMessage.proto Either.proto RhoTypes.proto 69 | $(PBJS) -t static-module -w commonjs --no-comments --keep-case -o $@ \ 70 | ProposeService.proto CasperMessage.proto Either.proto RhoTypes.proto 71 | 72 | RhoTypes.js: RhoTypes.proto 73 | $(PBJS) -t static-module -w commonjs --no-comments --keep-case -o $@ $< 74 | 75 | static-types: definitelytyped flowtyped 76 | 77 | flowtyped: $(STATIC_FLOW) 78 | 79 | .SUFFIXES: .proto .js .d.ts .js.flow 80 | 81 | %.d.ts: %.proto 82 | $(PBJS) -t static-module --keep-case $< | $(PBTS) -o $@ --no-comments - 83 | 84 | definitelytyped: $(STATIC_TS) 85 | 86 | CasperMessage.d.ts: CasperMessage.proto 87 | 88 | RhoTypes.d.ts: RhoTypes.proto 89 | 90 | Either.d.ts: Either.proto 91 | 92 | 93 | interfaces: 94 | mkdir -p interfaces 95 | 96 | interfaces/%.js.flow: %.d.ts 97 | $(FLOWGEN) -o $@ $< 98 | 99 | interfaces/CasperMessage.js.flow: CasperMessage.d.ts 100 | 101 | interfaces/RhoTypes.js.flow: RhoTypes.d.ts 102 | 103 | interfaces/Either.js.flow: Either.d.ts 104 | 105 | interfaces/protobufjs.js.flow: ../node_modules/protobufjs/index.d.ts 106 | $(FLOWGEN) -o $@ $< 107 | -------------------------------------------------------------------------------- /protobuf/ProposeService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package coop.rchain.casper.protocol; 3 | 4 | import "CasperMessage.proto"; 5 | import "google/protobuf/empty.proto"; 6 | import "Either.proto"; 7 | import "scalapb/scalapb.proto"; 8 | 9 | option (scalapb.options) = { 10 | package_name: "coop.rchain.casper.protocol" 11 | flat_package: true 12 | single_file: true 13 | }; 14 | 15 | service ProposeService { 16 | rpc propose(PrintUnmatchedSendsQuery) returns (Either) {} 17 | } 18 | 19 | message PrintUnmatchedSendsQuery { 20 | bool printUnmatchedSends = 1; 21 | } 22 | -------------------------------------------------------------------------------- /protobuf/RhoTypes.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * Rholang Term Structure 3 | * 4 | * The top level is `Par`. 5 | */ 6 | syntax = "proto3"; 7 | 8 | // See CapserMessage.proto if this is causing problems 9 | import "scalapb/scalapb.proto"; 10 | 11 | option java_package = "coop.rchain.models"; 12 | 13 | option (scalapb.options) = { 14 | import: "coop.rchain.models.BitSetBytesMapper.bitSetBytesMapper" 15 | import: "coop.rchain.models.ParSetTypeMapper.parSetESetTypeMapper" 16 | import: "coop.rchain.models.ParMapTypeMapper.parMapEMapTypeMapper" 17 | }; 18 | 19 | /** 20 | * Rholang process 21 | * 22 | * For example, `@0!(1) | @2!(3) | for(x <- @0) { Nil }` has two sends 23 | * and one receive. 24 | * 25 | * The Nil process is a `Par` with no sends, receives, etc. 26 | */ 27 | message Par { 28 | repeated Send sends = 1; 29 | repeated Receive receives = 2; 30 | repeated New news = 4; 31 | repeated Expr exprs = 5; 32 | repeated Match matches = 6; 33 | repeated GUnforgeable unforgeables = 7; // unforgeable names 34 | repeated Bundle bundles = 11; 35 | repeated Connective connectives = 8; 36 | bytes locallyFree = 9 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 37 | bool connective_used = 10; 38 | } 39 | 40 | /** 41 | * Either rholang code or code built in to the interpreter. 42 | */ 43 | message TaggedContinuation { 44 | oneof tagged_cont { 45 | ParWithRandom par_body = 1; 46 | int64 scala_body_ref = 2; 47 | } 48 | } 49 | 50 | /** 51 | * Rholang code along with the state of a split random number 52 | * generator for generating new unforgeable names. 53 | */ 54 | message ParWithRandom { 55 | Par body = 1 [(scalapb.field).no_box = true]; 56 | bytes randomState = 2 [(scalapb.field).type = "coop.rchain.crypto.hash.Blake2b512Random"]; 57 | } 58 | 59 | /** 60 | * Cost of the performed operations. 61 | */ 62 | message PCost { 63 | uint64 cost = 1; 64 | } 65 | 66 | message ListParWithRandom { 67 | repeated Par pars = 1; 68 | bytes randomState = 2 [(scalapb.field).type = "coop.rchain.crypto.hash.Blake2b512Random"]; 69 | } 70 | 71 | // While we use vars in both positions, when producing the normalized 72 | // representation we need a discipline to track whether a var is a name or a 73 | // process. 74 | // These are DeBruijn levels 75 | message Var { 76 | message WildcardMsg {} 77 | oneof var_instance { 78 | sint32 bound_var = 1; 79 | sint32 free_var = 2; 80 | WildcardMsg wildcard = 3; 81 | } 82 | } 83 | 84 | /** 85 | * Nothing can be received from a (quoted) bundle with `readFlag = false`. 86 | * Likeise nothing can be sent to a (quoted) bundle with `writeFlag = false`. 87 | * 88 | * If both flags are set to false, bundle allows only for equivalance check. 89 | */ 90 | message Bundle { 91 | Par body = 1 [(scalapb.field).no_box = true]; 92 | bool writeFlag = 2; // flag indicating whether bundle is writeable 93 | bool readFlag = 3; // flag indicating whether bundle is readable 94 | } 95 | 96 | /** 97 | * A send is written `chan!(data)` or `chan!!(data)` for a persistent send. 98 | * 99 | * Upon send, all free variables in data are substituted with their values. 100 | */ 101 | message Send { 102 | Par chan = 1 [(scalapb.field).no_box = true]; 103 | repeated Par data = 2; 104 | bool persistent = 3; 105 | bytes locallyFree = 5 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 106 | bool connective_used = 6; 107 | } 108 | 109 | message ReceiveBind { 110 | repeated Par patterns = 1; 111 | Par source = 2 [(scalapb.field).no_box = true]; 112 | Var remainder = 3; 113 | int32 freeCount = 4; 114 | } 115 | 116 | message BindPattern { 117 | repeated Par patterns = 1; 118 | Var remainder = 2; 119 | int32 freeCount = 3; 120 | } 121 | 122 | message ListBindPatterns { 123 | repeated BindPattern patterns = 1; 124 | } 125 | 126 | /** 127 | * A receive is written `for(binds) { body }` 128 | * i.e. `for(patterns <- source) { body }` 129 | * or for a persistent recieve: `for(patterns <= source) { body }`. 130 | * 131 | * It's an error for free Variable to occur more than once in a pattern. 132 | */ 133 | message Receive { 134 | repeated ReceiveBind binds = 1; 135 | Par body = 2 [(scalapb.field).no_box = true]; 136 | bool persistent = 3; 137 | bool peek = 4; 138 | int32 bindCount = 5; 139 | bytes locallyFree = 6 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 140 | bool connective_used = 7; 141 | } 142 | 143 | // Number of variables bound in the new statement. 144 | // For normalized form, p should not contain solely another new. 145 | // Also for normalized form, the first use should be level+0, next use level+1 146 | // up to level+count for the last used variable. 147 | message New { 148 | // Includes any uris listed below. This makes it easier to substitute or walk a term. 149 | sint32 bindCount = 1; 150 | Par p = 2 [(scalapb.field).no_box = true]; 151 | // For normalization, uri-referenced variables come at the end, and in lexicographical order. 152 | repeated string uri = 3; 153 | DeployId deployId = 4; 154 | DeployerId deployerId = 5; 155 | bytes locallyFree = 6 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 156 | } 157 | 158 | message MatchCase { 159 | Par pattern = 1 [(scalapb.field).no_box = true]; 160 | Par source = 2 [(scalapb.field).no_box = true]; 161 | int32 freeCount = 3; 162 | } 163 | 164 | message Match { 165 | Par target = 1 [(scalapb.field).no_box = true]; 166 | repeated MatchCase cases = 2; 167 | bytes locallyFree = 4 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 168 | bool connective_used = 5; 169 | } 170 | 171 | // Any process may be an operand to an expression. 172 | // Only processes equivalent to a ground process of compatible type will reduce. 173 | message Expr { 174 | oneof expr_instance { 175 | bool g_bool = 1; 176 | sint64 g_int = 2; 177 | string g_string = 3; 178 | string g_uri = 4; 179 | bytes g_byte_array = 25; 180 | 181 | ENot e_not_body = 5; 182 | ENeg e_neg_body = 6; 183 | EMult e_mult_body = 7; 184 | EDiv e_div_body = 8; 185 | EPlus e_plus_body = 9; 186 | EMinus e_minus_body = 10; 187 | ELt e_lt_body = 11; 188 | ELte e_lte_body = 12; 189 | EGt e_gt_body = 13; 190 | EGte e_gte_body = 14; 191 | EEq e_eq_body = 15; 192 | ENeq e_neq_body = 16; 193 | EAnd e_and_body = 17; 194 | EOr e_or_body = 18; 195 | EVar e_var_body = 19; 196 | 197 | EList e_list_body = 20; 198 | ETuple e_tuple_body = 21; 199 | ESet e_set_body = 22 [(scalapb.field).type = "coop.rchain.models.ParSet"]; 200 | EMap e_map_body = 23 [(scalapb.field).type = "coop.rchain.models.ParMap"]; 201 | EMethod e_method_body = 24; 202 | 203 | EMatches e_matches_body = 27; 204 | EPercentPercent e_percent_percent_body = 28; // string interpolation 205 | EPlusPlus e_plus_plus_body = 29; // concatenation 206 | EMinusMinus e_minus_minus_body = 30; // set difference 207 | 208 | EMod e_mod_body = 31; 209 | } 210 | } 211 | 212 | message EList { 213 | repeated Par ps = 1; 214 | bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 215 | bool connective_used = 4; 216 | Var remainder = 5; 217 | } 218 | 219 | message ETuple { 220 | repeated Par ps = 1; 221 | bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 222 | bool connective_used = 4; 223 | } 224 | 225 | message ESet { 226 | repeated Par ps = 1; 227 | bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 228 | bool connective_used = 4; 229 | Var remainder = 5; 230 | } 231 | 232 | message EMap { 233 | repeated KeyValuePair kvs = 1; 234 | bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 235 | bool connective_used = 4; 236 | Var remainder = 5; 237 | } 238 | 239 | /** 240 | * `target.method(arguments)` 241 | */ 242 | message EMethod { 243 | string methodName = 1; 244 | Par target = 2 [(scalapb.field).no_box = true]; 245 | repeated Par arguments = 3; 246 | bytes locallyFree = 5 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; 247 | bool connective_used = 6; 248 | } 249 | 250 | message KeyValuePair { 251 | Par key = 1 [(scalapb.field).no_box = true]; 252 | Par value = 2 [(scalapb.field).no_box = true]; 253 | } 254 | 255 | // A variable used as a var should be bound in a process context, not a name 256 | // context. For example: 257 | // `for (@x <- c1; @y <- c2) { z!(x + y) }` is fine, but 258 | // `for (x <- c1; y <- c2) { z!(x + y) }` should raise an error. 259 | message EVar { 260 | Var v = 1 [(scalapb.field).no_box = true]; 261 | } 262 | 263 | message ENot { 264 | Par p = 1 [(scalapb.field).no_box = true]; 265 | } 266 | 267 | message ENeg { 268 | Par p = 1 [(scalapb.field).no_box = true]; 269 | } 270 | 271 | message EMult { 272 | Par p1 = 1 [(scalapb.field).no_box = true]; 273 | Par p2 = 2 [(scalapb.field).no_box = true]; 274 | } 275 | 276 | message EDiv { 277 | Par p1 = 1 [(scalapb.field).no_box = true]; 278 | Par p2 = 2 [(scalapb.field).no_box = true]; 279 | } 280 | 281 | message EMod { 282 | Par p1 = 1 [(scalapb.field).no_box = true]; 283 | Par p2 = 2 [(scalapb.field).no_box = true]; 284 | } 285 | 286 | message EPlus { 287 | Par p1 = 1 [(scalapb.field).no_box = true]; 288 | Par p2 = 2 [(scalapb.field).no_box = true]; 289 | } 290 | 291 | message EMinus { 292 | Par p1 = 1 [(scalapb.field).no_box = true]; 293 | Par p2 = 2 [(scalapb.field).no_box = true]; 294 | } 295 | 296 | message ELt { 297 | Par p1 = 1 [(scalapb.field).no_box = true]; 298 | Par p2 = 2 [(scalapb.field).no_box = true]; 299 | } 300 | 301 | message ELte { 302 | Par p1 = 1 [(scalapb.field).no_box = true]; 303 | Par p2 = 2 [(scalapb.field).no_box = true]; 304 | } 305 | 306 | message EGt { 307 | Par p1 = 1 [(scalapb.field).no_box = true]; 308 | Par p2 = 2 [(scalapb.field).no_box = true]; 309 | } 310 | 311 | message EGte { 312 | Par p1 = 1 [(scalapb.field).no_box = true]; 313 | Par p2 = 2 [(scalapb.field).no_box = true]; 314 | } 315 | 316 | message EEq { 317 | Par p1 = 1 [(scalapb.field).no_box = true]; 318 | Par p2 = 2 [(scalapb.field).no_box = true]; 319 | } 320 | 321 | message ENeq { 322 | Par p1 = 1 [(scalapb.field).no_box = true]; 323 | Par p2 = 2 [(scalapb.field).no_box = true]; 324 | } 325 | 326 | message EAnd { 327 | Par p1 = 1 [(scalapb.field).no_box = true]; 328 | Par p2 = 2 [(scalapb.field).no_box = true]; 329 | } 330 | 331 | message EOr { 332 | Par p1 = 1 [(scalapb.field).no_box = true]; 333 | Par p2 = 2 [(scalapb.field).no_box = true]; 334 | } 335 | 336 | message EMatches { 337 | Par target = 1 [(scalapb.field).no_box = true]; 338 | Par pattern = 2 [(scalapb.field).no_box = true]; 339 | } 340 | 341 | /** 342 | * String interpolation 343 | * 344 | * `"Hello, {name}" %% {"name": "Bob"}` denotes `"Hello, Bob"` 345 | */ 346 | message EPercentPercent { 347 | Par p1 = 1 [(scalapb.field).no_box = true]; 348 | Par p2 = 2 [(scalapb.field).no_box = true]; 349 | } 350 | 351 | // Concatenation 352 | message EPlusPlus { 353 | Par p1 = 1 [(scalapb.field).no_box = true]; 354 | Par p2 = 2 [(scalapb.field).no_box = true]; 355 | } 356 | 357 | // Set difference 358 | message EMinusMinus { 359 | Par p1 = 1 [(scalapb.field).no_box = true]; 360 | Par p2 = 2 [(scalapb.field).no_box = true]; 361 | } 362 | 363 | message Connective { 364 | oneof connective_instance { 365 | ConnectiveBody conn_and_body = 1; 366 | ConnectiveBody conn_or_body = 2; 367 | Par conn_not_body = 3; 368 | VarRef var_ref_body = 4; 369 | bool conn_bool = 5; 370 | bool conn_int = 6; 371 | bool conn_string = 7; 372 | bool conn_uri = 8; 373 | bool conn_byte_array = 9; 374 | } 375 | } 376 | 377 | message VarRef { 378 | sint32 index = 1; 379 | sint32 depth = 2; 380 | } 381 | 382 | message ConnectiveBody { 383 | repeated Par ps = 1; 384 | } 385 | 386 | message DeployId { 387 | bytes sig = 1; 388 | } 389 | 390 | message DeployerId { 391 | bytes publicKey = 1; 392 | } 393 | 394 | // Unforgeable names resulting from `new x { ... }` 395 | // These should only occur as the program is being evaluated. There is no way in 396 | // the grammar to construct them. 397 | message GUnforgeable { 398 | oneof unf_instance { 399 | GPrivate g_private_body = 1; 400 | GDeployId g_deploy_id_body = 2; 401 | GDeployerId g_deployer_id_body = 3; 402 | } 403 | } 404 | message GPrivate { 405 | bytes id = 1; 406 | } 407 | 408 | message GDeployId { 409 | bytes sig = 1; 410 | } 411 | 412 | message GDeployerId { 413 | bytes publicKey = 1; 414 | } 415 | -------------------------------------------------------------------------------- /protobuf/google/protobuf/any.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "github.com/golang/protobuf/ptypes/any"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "AnyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | 42 | // `Any` contains an arbitrary serialized protocol buffer message along with a 43 | // URL that describes the type of the serialized message. 44 | // 45 | // Protobuf library provides support to pack/unpack Any values in the form 46 | // of utility functions or additional generated methods of the Any type. 47 | // 48 | // Example 1: Pack and unpack a message in C++. 49 | // 50 | // Foo foo = ...; 51 | // Any any; 52 | // any.PackFrom(foo); 53 | // ... 54 | // if (any.UnpackTo(&foo)) { 55 | // ... 56 | // } 57 | // 58 | // Example 2: Pack and unpack a message in Java. 59 | // 60 | // Foo foo = ...; 61 | // Any any = Any.pack(foo); 62 | // ... 63 | // if (any.is(Foo.class)) { 64 | // foo = any.unpack(Foo.class); 65 | // } 66 | // 67 | // Example 3: Pack and unpack a message in Python. 68 | // 69 | // foo = Foo(...) 70 | // any = Any() 71 | // any.Pack(foo) 72 | // ... 73 | // if any.Is(Foo.DESCRIPTOR): 74 | // any.Unpack(foo) 75 | // ... 76 | // 77 | // Example 4: Pack and unpack a message in Go 78 | // 79 | // foo := &pb.Foo{...} 80 | // any, err := ptypes.MarshalAny(foo) 81 | // ... 82 | // foo := &pb.Foo{} 83 | // if err := ptypes.UnmarshalAny(any, foo); err != nil { 84 | // ... 85 | // } 86 | // 87 | // The pack methods provided by protobuf library will by default use 88 | // 'type.googleapis.com/full.type.name' as the type URL and the unpack 89 | // methods only use the fully qualified type name after the last '/' 90 | // in the type URL, for example "foo.bar.com/x/y.z" will yield type 91 | // name "y.z". 92 | // 93 | // 94 | // JSON 95 | // ==== 96 | // The JSON representation of an `Any` value uses the regular 97 | // representation of the deserialized, embedded message, with an 98 | // additional field `@type` which contains the type URL. Example: 99 | // 100 | // package google.profile; 101 | // message Person { 102 | // string first_name = 1; 103 | // string last_name = 2; 104 | // } 105 | // 106 | // { 107 | // "@type": "type.googleapis.com/google.profile.Person", 108 | // "firstName": , 109 | // "lastName": 110 | // } 111 | // 112 | // If the embedded message type is well-known and has a custom JSON 113 | // representation, that representation will be embedded adding a field 114 | // `value` which holds the custom JSON in addition to the `@type` 115 | // field. Example (for message [google.protobuf.Duration][]): 116 | // 117 | // { 118 | // "@type": "type.googleapis.com/google.protobuf.Duration", 119 | // "value": "1.212s" 120 | // } 121 | // 122 | message Any { 123 | // A URL/resource name whose content describes the type of the 124 | // serialized protocol buffer message. 125 | // 126 | // For URLs which use the scheme `http`, `https`, or no scheme, the 127 | // following restrictions and interpretations apply: 128 | // 129 | // * If no scheme is provided, `https` is assumed. 130 | // * The last segment of the URL's path must represent the fully 131 | // qualified name of the type (as in `path/google.protobuf.Duration`). 132 | // The name should be in a canonical form (e.g., leading "." is 133 | // not accepted). 134 | // * An HTTP GET on the URL must yield a [google.protobuf.Type][] 135 | // value in binary format, or produce an error. 136 | // * Applications are allowed to cache lookup results based on the 137 | // URL, or have them precompiled into a binary to avoid any 138 | // lookup. Therefore, binary compatibility needs to be preserved 139 | // on changes to types. (Use versioned type names to manage 140 | // breaking changes.) 141 | // 142 | // Schemes other than `http`, `https` (or the empty scheme) might be 143 | // used with implementation specific semantics. 144 | // 145 | string type_url = 1; 146 | 147 | // Must be a valid serialized protocol buffer of the above specified type. 148 | bytes value = 2; 149 | } 150 | -------------------------------------------------------------------------------- /protobuf/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "github.com/golang/protobuf/ptypes/empty"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} 53 | -------------------------------------------------------------------------------- /protobuf/interfaces/Either.js.flow: -------------------------------------------------------------------------------- 1 | /** 2 | * Flowtype definitions for Either 3 | * Generated by Flowgen from a Typescript Definition 4 | * Flowgen v1.8.0 5 | * Author: [Joar Wilk](http://twitter.com/joarwilk) 6 | * Repo: http://github.com/joarwilk/flowgen 7 | */ 8 | 9 | import * as $protobuf from "protobufjs"; 10 | export interface IEitherAny { 11 | type_url?: string | null; 12 | value?: Uint8Array | null; 13 | } 14 | declare export class EitherAny implements IEitherAny { 15 | constructor(properties?: IEitherAny): this; 16 | type_url: string; 17 | value: Uint8Array; 18 | static create(properties?: IEitherAny): EitherAny; 19 | static encode( 20 | message: IEitherAny, 21 | writer?: $protobuf.Writer 22 | ): $protobuf.Writer; 23 | static encodeDelimited( 24 | message: IEitherAny, 25 | writer?: $protobuf.Writer 26 | ): $protobuf.Writer; 27 | static decode( 28 | reader: $protobuf.Reader | Uint8Array, 29 | length?: number 30 | ): EitherAny; 31 | static decodeDelimited(reader: $protobuf.Reader | Uint8Array): EitherAny; 32 | static verify(message: { 33 | [k: string]: any 34 | }): string | null; 35 | static fromObject(object: { 36 | [k: string]: any 37 | }): EitherAny; 38 | static toObject( 39 | message: EitherAny, 40 | options?: $protobuf.IConversionOptions 41 | ): { 42 | [k: string]: any 43 | }; 44 | toJSON(): { 45 | [k: string]: any 46 | }; 47 | } 48 | export interface IEitherError { 49 | messages?: string[] | null; 50 | } 51 | declare export class EitherError implements IEitherError { 52 | constructor(properties?: IEitherError): this; 53 | messages: string[]; 54 | static create(properties?: IEitherError): EitherError; 55 | static encode( 56 | message: IEitherError, 57 | writer?: $protobuf.Writer 58 | ): $protobuf.Writer; 59 | static encodeDelimited( 60 | message: IEitherError, 61 | writer?: $protobuf.Writer 62 | ): $protobuf.Writer; 63 | static decode( 64 | reader: $protobuf.Reader | Uint8Array, 65 | length?: number 66 | ): EitherError; 67 | static decodeDelimited(reader: $protobuf.Reader | Uint8Array): EitherError; 68 | static verify(message: { 69 | [k: string]: any 70 | }): string | null; 71 | static fromObject(object: { 72 | [k: string]: any 73 | }): EitherError; 74 | static toObject( 75 | message: EitherError, 76 | options?: $protobuf.IConversionOptions 77 | ): { 78 | [k: string]: any 79 | }; 80 | toJSON(): { 81 | [k: string]: any 82 | }; 83 | } 84 | export interface IEitherSuccess { 85 | response?: IEitherAny | null; 86 | } 87 | declare export class EitherSuccess implements IEitherSuccess { 88 | constructor(properties?: IEitherSuccess): this; 89 | response: IEitherAny | null; 90 | static create(properties?: IEitherSuccess): EitherSuccess; 91 | static encode( 92 | message: IEitherSuccess, 93 | writer?: $protobuf.Writer 94 | ): $protobuf.Writer; 95 | static encodeDelimited( 96 | message: IEitherSuccess, 97 | writer?: $protobuf.Writer 98 | ): $protobuf.Writer; 99 | static decode( 100 | reader: $protobuf.Reader | Uint8Array, 101 | length?: number 102 | ): EitherSuccess; 103 | static decodeDelimited(reader: $protobuf.Reader | Uint8Array): EitherSuccess; 104 | static verify(message: { 105 | [k: string]: any 106 | }): string | null; 107 | static fromObject(object: { 108 | [k: string]: any 109 | }): EitherSuccess; 110 | static toObject( 111 | message: EitherSuccess, 112 | options?: $protobuf.IConversionOptions 113 | ): { 114 | [k: string]: any 115 | }; 116 | toJSON(): { 117 | [k: string]: any 118 | }; 119 | } 120 | export interface IEither { 121 | error?: IEitherError | null; 122 | success?: IEitherSuccess | null; 123 | } 124 | declare export class Either implements IEither { 125 | constructor(properties?: IEither): this; 126 | error: IEitherError | null; 127 | success: IEitherSuccess | null; 128 | content: "error" | "success"; 129 | static create(properties?: IEither): Either; 130 | static encode(message: IEither, writer?: $protobuf.Writer): $protobuf.Writer; 131 | static encodeDelimited( 132 | message: IEither, 133 | writer?: $protobuf.Writer 134 | ): $protobuf.Writer; 135 | static decode(reader: $protobuf.Reader | Uint8Array, length?: number): Either; 136 | static decodeDelimited(reader: $protobuf.Reader | Uint8Array): Either; 137 | static verify(message: { 138 | [k: string]: any 139 | }): string | null; 140 | static fromObject(object: { 141 | [k: string]: any 142 | }): Either; 143 | static toObject( 144 | message: Either, 145 | options?: $protobuf.IConversionOptions 146 | ): { 147 | [k: string]: any 148 | }; 149 | toJSON(): { 150 | [k: string]: any 151 | }; 152 | } 153 | declare var google: typeof npm$namespace$google; 154 | 155 | declare var npm$namespace$google: { 156 | protobuf: typeof npm$namespace$google$protobuf 157 | }; 158 | 159 | declare var npm$namespace$google$protobuf: { 160 | Any: typeof google$protobuf$Any 161 | }; 162 | declare interface google$protobuf$IAny { 163 | +type_url?: string | null; 164 | +value?: Uint8Array | null; 165 | } 166 | 167 | declare class google$protobuf$Any implements google$protobuf$IAny { 168 | constructor(properties?: google$protobuf$IAny): this; 169 | type_url: string; 170 | value: Uint8Array; 171 | static create(properties?: google$protobuf$IAny): google$protobuf$Any; 172 | static encode( 173 | message: google$protobuf$IAny, 174 | writer?: $protobuf.Writer 175 | ): $protobuf.Writer; 176 | static encodeDelimited( 177 | message: google$protobuf$IAny, 178 | writer?: $protobuf.Writer 179 | ): $protobuf.Writer; 180 | static decode( 181 | reader: $protobuf.Reader | Uint8Array, 182 | length?: number 183 | ): google$protobuf$Any; 184 | static decodeDelimited( 185 | reader: $protobuf.Reader | Uint8Array 186 | ): google$protobuf$Any; 187 | static verify(message: { 188 | [k: string]: any 189 | }): string | null; 190 | static fromObject(object: { 191 | [k: string]: any 192 | }): google$protobuf$Any; 193 | static toObject( 194 | message: google$protobuf$Any, 195 | options?: $protobuf.IConversionOptions 196 | ): { 197 | [k: string]: any 198 | }; 199 | toJSON(): { 200 | [k: string]: any 201 | }; 202 | } 203 | -------------------------------------------------------------------------------- /protobuf/scalapb/scalapb.proto: -------------------------------------------------------------------------------- 1 | // empty file satisfies includes from CasperMessages.proto etc. 2 | // but avoids: 3 | // Error: unresolvable extensions: 'extend google.protobuf.FileOptions' in .scalapb ... 4 | -------------------------------------------------------------------------------- /rclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rclient", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "src/main.js", 6 | "bin": { 7 | "rclient": "src/main.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Dan Connolly", 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "base32-encoding": "^1.0.0", 16 | "docopt": "^0.6.2", 17 | "grpc": "^1.17.0", 18 | "rchain-api": "file:..", 19 | "read": "^1.0.7", 20 | "scrypt.js": "^0.3.0", 21 | "secp256k1": "^3.6.1", 22 | "tweetnacl": "^1.0.0", 23 | "uuid": "^3.3.2" 24 | }, 25 | "devDependencies": { 26 | "flow-bin": "^0.91.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rclient/src/asPromise.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | 3 | // @flow 4 | 5 | /*:: 6 | type Callback = (any, T) => void; 7 | type WithCallback = Callback => void; 8 | */ 9 | 10 | exports.asPromise = asPromise; 11 | function asPromise/*:: */(calling /*: WithCallback*/) /*: Promise */{ 12 | function executor(resolve, reject) { 13 | const callback = (err, result) => { 14 | if (err) { reject(err); } 15 | resolve(result); 16 | }; 17 | 18 | calling(callback); 19 | } 20 | 21 | return new Promise(executor); 22 | } 23 | -------------------------------------------------------------------------------- /rclient/src/assets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * assets - treat text files as link-time artifacts. 3 | * 4 | * With respect to ocap discipline, we regard this as part of the 5 | * module loading infrastructure rather than a run-time operation. 6 | * 7 | * An alternative implementation would be a pre-processor 8 | * that in-lines the contents of the linked data as constants. 9 | */ 10 | // @flow 11 | /* global exports, require */ 12 | const { readFileSync } = require('fs'); 13 | 14 | exports.link = link; 15 | function link(name /*: string */) { 16 | return readFileSync(require.resolve(name), 'utf8'); 17 | } 18 | -------------------------------------------------------------------------------- /rclient/src/main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** rclient -- CLI interface to gRPC API 3 | */ 4 | // @flow 5 | /*global require, module, Buffer */ 6 | 7 | const assert = require('assert'); 8 | const read = require('read'); 9 | 10 | const secp256k1 = require('secp256k1'); // ISSUE: push into rchain-api? 11 | const { docopt } = require('docopt'); 12 | const { 13 | RNode, RHOCore, REV, 14 | RholangCrypto, 15 | Hex, Ed25519keyPair, 16 | RegistryProxy, 17 | } = require('rchain-api'); 18 | const { loadRhoModules } = require('../../src/loading'); // ISSUE: path? 19 | 20 | const { fsReadAccess, fsWriteAccess, FileStorage } = require('./pathlib'); 21 | const { asPromise } = require('./asPromise'); 22 | const secretStorage = require('./secretStorage'); 23 | const { link } = require('./assets'); 24 | 25 | const { SignDeployment, RevAddress } = REV; 26 | const { keccak256Hash } = RholangCrypto; 27 | const { makeProxy } = RegistryProxy; 28 | const h2b = Hex.decode; 29 | const b2h = Hex.encode; 30 | 31 | const usage = ` 32 | Usage: 33 | rclient [options] keygen LABEL 34 | rclient [options] import LABEL JSONFILE 35 | rclient [options] info LABEL 36 | rclient [options] genVault LABEL AMOUNT 37 | rclient [options] balance LABEL 38 | rclient [options] transfer --from=LABEL --to=REVADDR AMOUNT 39 | rclient [options] sign LABEL [ --json ] DATAFILE 40 | rclient [options] deploy RHOLANG 41 | rclient [options] register LABEL RHOMODULE... 42 | 43 | Options: 44 | keygen generate new secret key 45 | and associate with LABEL in keystore 46 | --keystore=FILE [default: keystore.json] 47 | import import key from JSONFILE as LABEL 48 | (per ethereum Web3 Secret Storage) 49 | info show public key and ethereum-style address 50 | after decrypting with password 51 | genVault create a genesis vault (testnet only) 52 | claim claim REV balance from RHOC after genesis 53 | publish register a wallet at the rhoid derived 54 | from the key. If --claimed, get the wallet 55 | from WalletCheck; else, create a new empty wallet. 56 | balance get REV balance after publishing account 57 | --host NAME The hostname or IPv4 address of the node 58 | [default: localhost] 59 | --port INT The tcp port of the nodes gRPC service 60 | [default: 40401] 61 | --phlo-limit=N how much are you willing to spend? [default: 1000000] 62 | --phlo-price=N TODO docs [default: 1] 63 | deploy deploy RHOLANG file 64 | register deploy RHOMODULE, register exported process, 65 | and save URI in registry file 66 | --poll-interval=N when listening for data after a call. in ms [default: 5000] 67 | --registry=FILE where to store file / URI mappings 68 | [default: registry.json] 69 | -v --verbose Verbose logging 70 | -h --help show usage 71 | 72 | `; 73 | 74 | /*:: 75 | import type { SecretStorageV3, AES128CTR, SCrypt } from './secretStorage'; 76 | import { Base16 } from './codec.js'; 77 | import type { ModuleInfo } from '../../src/loading'; // ISSUE: path? 78 | 79 | import type { HexStr, PublicKey } from 'rchain-api'; 80 | */ 81 | 82 | function ExitStatus(message) { 83 | this.message = message; 84 | } 85 | 86 | async function main( 87 | argv, 88 | { stdin, stdout, writeFile, readFile, join }, 89 | { clock, randomBytes, setTimeout }, 90 | { grpc, uuidv4 }, 91 | ) { 92 | const cli = docopt(usage, { argv: argv.slice(2) }); 93 | if (cli['--verbose']) { console.log('options:', argv, cli); } 94 | 95 | const rd = path => fsReadAccess(path, readFile, join); 96 | const argRd = arg => rd(cli[arg]); 97 | const argWr = arg => fsWriteAccess(cli[arg], writeFile, readFile, join); 98 | function argInt(name) { 99 | try { 100 | return parseInt(cli[name], 10); 101 | } catch (oops) { 102 | console.error(oops.message); 103 | throw oops; 104 | } 105 | } 106 | 107 | function getpass(prompt /*: string*/) { 108 | return asPromise( 109 | f => read({ input: stdin, output: stdout, silent: true, prompt }, f), 110 | ); 111 | } 112 | 113 | const where = { host: cli['--host'], port: argInt('--port') }; 114 | const rnode = RNode(grpc, where); 115 | const mkPause = makeTimer(setTimeout); 116 | const dur = argInt('--poll-interval'); 117 | const delay = i => mkPause(dur * i); 118 | 119 | function payWith(key) { 120 | return function payFor(d0) { 121 | return SignDeployment.sign(key, { 122 | ...d0, 123 | phloPrice: argInt('--phlo-price'), 124 | phloLimit: argInt('--phlo-limit'), 125 | }); 126 | }; 127 | } 128 | 129 | async function ioTools() { 130 | const registry = FileStorage(argWr('--registry')); 131 | const toolsMod = await ensureLoaded('tools.rho', link('./tools.rho'), { registry }); 132 | return { getpass, rnode, clock, delay, toolsMod, registry, keyStore: argWr('--keystore') }; 133 | } 134 | 135 | if (cli.keygen) { 136 | await keygen(argWr('--keystore'), cli.LABEL, { getpass, randomBytes, uuidv4 }); 137 | } else if (cli.import) { 138 | await importKey(argWr('--keystore'), cli.LABEL, argRd('JSONFILE'), { getpass }); 139 | } else if (cli.info) { 140 | // ISSUE: store un-encrypted public key? "compromises privacy" per Web3 docs. 141 | 142 | // ISSUE: we only need read-only access to the key store; 143 | // should WriteAccess extend ReadAccess? 144 | await showPublic(cli.LABEL, { getpass, keyStore: argWr('--keystore') }); 145 | } else if (cli.genVault) { 146 | const io = await ioTools(); 147 | await genVault(cli.LABEL, argInt('AMOUNT'), payWith, io); 148 | } else if (cli.balance) { 149 | const io = await ioTools(); 150 | await getBalance(cli.LABEL, payWith, io); 151 | } else if (cli.transfer) { 152 | const io = await ioTools(); 153 | await transferPayment(argInt('AMOUNT'), cli['--from'], cli['--to'], payWith, io); 154 | } else if (cli.sign) { 155 | const input = { data: argRd('DATAFILE'), json: cli['--json'] }; 156 | await signMessage(argWr('--keystore'), cli.LABEL, input, { getpass }); 157 | } else if (cli.deploy) { 158 | await deploy(argRd('RHOLANG'), payWith, where, { rnode, clock }) 159 | .catch((err) => { console.error(err); throw err; }); 160 | } else if (cli.register) { 161 | const registry = FileStorage(argWr('--registry')); 162 | await register( 163 | cli.LABEL, cli.RHOMODULE.map(rd), payWith, 164 | { getpass, rnode, clock, delay, registry, keyStore: argWr('--keystore') }, 165 | ); 166 | } 167 | } 168 | 169 | function makeTimer(setTimeout) { 170 | return function timer(ms) { 171 | console.log(`making pause of ${ms} ms`); 172 | return new Promise(resolve => setTimeout(() => { 173 | console.log(`${ms} pause done`); 174 | resolve(); 175 | }, ms)); 176 | }; 177 | } 178 | 179 | 180 | async function deploy(rholang, price, where, { rnode, clock }) { 181 | const term = await rholang.readText(); 182 | const timestamp = clock().valueOf(); 183 | try { 184 | const msg = await rnode.doDeploy({ term, timestamp, nonce: 1, ...price }); 185 | console.log(msg); 186 | } catch (oops) { 187 | console.log(`failed to deploy ${rholang.name()} to ${where.host}:${where.port}: ${oops.message}`); 188 | } 189 | } 190 | 191 | 192 | async function register( 193 | label, files, payWith, 194 | { registry, rnode, clock, delay, keyStore, getpass }, 195 | ) { 196 | // ISSUE: what to do when we restart the node? 197 | // how to check that we're talking to the same chain? 198 | async function check1(file) { 199 | const src = await ioOrExit(file.readText()); 200 | 201 | const srcHash = RHOCore.wrapHash(keccak256Hash)(src); 202 | const mods = await ioOrExit(registry.get(srcHash)); 203 | return { file, src, srcHash, mod: mods[srcHash] }; 204 | } 205 | 206 | const status = await Promise.all(files.map(check1)); 207 | status.filter(({ mod }) => !!mod).forEach(({ file, mod }) => { 208 | console.log({ file: file.name(), uri: mod.URI, status: 'loaded already' }); 209 | }); 210 | 211 | const toLoad = status.filter(({ mod }) => !mod); 212 | if (toLoad.length > 0) { 213 | console.log('loading:', toLoad.map(({ file }) => file.name())); 214 | const { signingKey } = await loadRevAddr(label, [], { keyStore, getpass }); 215 | const loaded = await ioOrExit( 216 | loadRhoModules(toLoad.map(({ src }) => src), payWith(signingKey), { rnode, clock, delay }), 217 | ); 218 | registry.set(collect(loaded.map((m, ix) => [toLoad[ix].srcHash, m]))); 219 | loaded.forEach((m, ix) => { 220 | console.log({ status: 'newly loaded', file: toLoad[ix].file.name(), uri: String(m.URI) }); 221 | }); 222 | } 223 | } 224 | 225 | // [[p1, v1], [p2, v2], ...] => { p1: v1, p2: v2, ... } 226 | function collect/*:: */(props /*: [string, T][]*/) /*{ [string]: T } */{ 227 | return props.reduce((acc, [s, m]) => ({ ...acc, [s]: m }), {}); 228 | } 229 | 230 | async function ioOrExit/*::*/(p /*: Promise*/) /*: Promise*/ { 231 | try { 232 | return await p; 233 | } catch (err) { 234 | throw new ExitStatus(err.message); 235 | } 236 | } 237 | 238 | 239 | async function keygen(keyStore, label, { getpass, randomBytes, uuidv4 }) { 240 | const store = FileStorage(keyStore); 241 | 242 | const taken = await store.get(label); 243 | if (taken[label]) { throw new ExitStatus(`Key ${label} already exists.`); } 244 | 245 | const password = await getpass(`Password for ${label}:`); 246 | const passconf = await getpass(`Confirm password for ${label}:`); 247 | if (password !== passconf) { throw new ExitStatus('Passwords do not match.'); } 248 | 249 | let privKey; 250 | do { 251 | privKey = randomBytes(32); 252 | } while (!secp256k1.privateKeyVerify(privKey)); 253 | const item = secretStorage.encrypt( 254 | privKey, 255 | Buffer.from(password), 256 | n => randomBytes(n), 257 | uuidv4, 258 | ); 259 | await store.set({ [label]: item }); 260 | const publicKey /*: HexStr */= Ed25519keyPair(privKey).publicKey(); 261 | const revAddr = RevAddress.fromPublicKey(h2b(publicKey)).toString(); 262 | console.log({ label, revAddr, publicKey, keyStore: keyStore.readOnly().name(), status: 'saved' }); 263 | } 264 | 265 | 266 | async function importKey(keyStore, label, jsonKeyfile, { getpass }) { 267 | const store = FileStorage(keyStore); 268 | 269 | const password = await getpass(`Password for ${jsonKeyfile.name()}: `); 270 | const code = await jsonKeyfile.readText(); 271 | const item = JSON.parse(code); 272 | 273 | if (item.Crypto) { 274 | item.crypto = item.Crypto; 275 | delete item.Crypto; 276 | } 277 | if (item.crypto.kdf !== 'scrypt') { 278 | throw new ExitStatus(`unsupported kdf: ${item.crypto.kdf}`); 279 | } 280 | if (item.crypto.cipher !== 'aes-128-ctr') { 281 | throw new ExitStatus(`unsupported cipher: ${item.crypto.cipher}`); 282 | } 283 | 284 | try { 285 | const privKey = secretStorage.decrypt(Buffer.from(password), item); 286 | const pubKey = privateToPublic(privKey); 287 | const ethAddr = `0x${b2h(pubToAddress(pubKey))}`; 288 | await store.set({ [label]: item }); 289 | console.log({ label, ethAddr, keyStore: keyStore.readOnly().name() }); 290 | } catch (err) { 291 | throw new ExitStatus(`cannot import key: ${err.message}`); 292 | } 293 | } 294 | 295 | 296 | async function loadKey(keyStore, label, notice, { getpass }) /*: Promise */{ 297 | const store = FileStorage(keyStore); 298 | 299 | const keys = await ioOrExit(store.get(label)); 300 | if (!keys[label]) { throw new ExitStatus(`No such key: ${label}.`); } 301 | 302 | // ISSUE: logging is not just FYI here; 303 | // should be passed as an explicit capability. 304 | notice.forEach((args) => { 305 | console.log(...args); 306 | }); 307 | 308 | const password = await getpass(`Password for ${label}: `); 309 | 310 | // $FlowFixMe$ TODO: mixed -> SecretStorage 311 | const item /*: SecretStorageV3*/ = keys[label]; 312 | const privKey = secretStorage.decrypt(Buffer.from(password), item); 313 | return privKey; 314 | } 315 | 316 | 317 | async function signMessage(keyStore, label, input, { getpass }) { 318 | let message; 319 | let notice = []; 320 | if (input.json) { 321 | const code = await input.data.readText(); 322 | const data = JSON.parse(code); 323 | const par = RHOCore.fromJSData(data); 324 | const rholang = RHOCore.toRholang(par); 325 | notice = [ 326 | ['JavaScript data:'], 327 | [data], 328 | ['Rholang data:'], 329 | [rholang], 330 | ]; 331 | message = RHOCore.toByteArray(par); 332 | } else { 333 | message = await input.data.readBytes(); 334 | } 335 | notice.push(['byte length:', message.length]); 336 | 337 | try { 338 | const privKey = await loadKey(keyStore, label, notice, { getpass }); 339 | const sigObj = secp256k1.sign(Base16(message), privKey); 340 | console.log(b2h(sigObj.signature)); 341 | } catch (oops) { 342 | console.error('cannot load key'); 343 | console.error(oops.message); 344 | } 345 | } 346 | 347 | 348 | async function showPublic(label, { keyStore, getpass }) { 349 | const { revAddr, publicKey } = await loadRevAddr(label, [], { keyStore, getpass }); 350 | console.log({ label, publicKey, revAddr }); 351 | } 352 | 353 | // ISSUE: move to secretStorage 354 | function privateToPublic(privKey) { 355 | const der = secp256k1.publicKeyCreate(privKey, false); 356 | assert.equal(der.length, 65); 357 | // chop off 0x04 DER byte sequence tag? 358 | return der.slice(1); 359 | } 360 | 361 | function pubToAddress(pubKey) { 362 | assert.equal(pubKey.length, 64); 363 | return keccak256Hash(pubKey).slice(-20); 364 | } 365 | 366 | 367 | async function loadRevAddr(label, notice, { keyStore, getpass }) { 368 | try { 369 | const privKey = await loadKey(keyStore, label, notice, { getpass }); 370 | const edKey = Ed25519keyPair(privKey); 371 | const revAddr = RevAddress.fromPublicKey(h2b(edKey.publicKey())).toString(); 372 | return { label, revAddr, signingKey: edKey, publicKey: edKey.publicKey() }; 373 | } catch (err) { 374 | throw new ExitStatus(`cannot load public key: ${err.message}`); 375 | } 376 | } 377 | 378 | async function genVault( 379 | label, amount, payWith, 380 | { keyStore, getpass, toolsMod, rnode, clock, delay }, 381 | ) { 382 | const { revAddr, signingKey, publicKey } = await loadRevAddr(label, [], { keyStore, getpass }); 383 | 384 | const tools = makeProxy( 385 | toolsMod.URI, h2b(publicKey), payWith(signingKey), 386 | { rnode, clock, delay }, 387 | ); 388 | const result = await tools.genVault(revAddr, amount); 389 | if (result && typeof result === 'object' && typeof result.message === 'string') { 390 | throw new ExitStatus(`cannot generate vault: ${result.message}`); 391 | } 392 | console.log({ revAddr, label, amount, result }); 393 | } 394 | 395 | 396 | async function getBalance(label, payWith, { keyStore, toolsMod, getpass, rnode, clock, delay }) { 397 | const { revAddr, publicKey, signingKey } = await loadRevAddr(label, [], { keyStore, getpass }); 398 | 399 | const tools = makeProxy( 400 | toolsMod.URI, h2b(publicKey), payWith(signingKey), 401 | { rnode, clock, delay }, 402 | ); 403 | const balance = await tools.balance(revAddr); 404 | 405 | console.log({ revAddr, balance, label }); 406 | } 407 | 408 | async function transferPayment( 409 | amount, fromLabel, toAddr, payWith, 410 | { keyStore, toolsMod, getpass, rnode, clock, delay }, 411 | ) { 412 | const notice = [[`send ${String(amount)} from ${fromLabel} to ${String(toAddr)}`]]; 413 | try { 414 | RevAddress.parse(toAddr); 415 | } catch (badAddr) { 416 | throw new ExitStatus(`bad destination address: ${badAddr.message}`); 417 | } 418 | 419 | const { revAddr, publicKey, signingKey } = await loadRevAddr( 420 | fromLabel, notice, 421 | { keyStore, getpass }, 422 | ); 423 | 424 | const tools = makeProxy( 425 | toolsMod.URI, h2b(publicKey), payWith(signingKey), 426 | { rnode, clock, delay }, 427 | ); 428 | 429 | const transferResult = await tools.transfer(revAddr, toAddr, amount); 430 | console.log({ transferResult }); 431 | } 432 | 433 | 434 | /*:: 435 | type WebSendResult = { "=": T } | { "!": string } 436 | */ 437 | 438 | // waterken JSON convensions http://waterken.sourceforge.net/web_send/ 439 | async function outcome/*::*/(x /*:Promise*/) /*: Promise*/ { 440 | let o; 441 | try { 442 | o = await x; 443 | } catch (err) { 444 | throw new ExitStatus(`out of phlogistons? ${err.message}`); 445 | } 446 | if (o === null || typeof o !== 'object') { throw new Error('bad reply'); } 447 | if ('!' in o) { throw new ExitStatus(o['!']); } 448 | // $FlowFixMe validate mixed -> T 449 | return o['=']; 450 | } 451 | 452 | async function ensureLoaded(name, src, { registry }) /*: ModuleInfo */ { 453 | const srcHash = RHOCore.wrapHash(keccak256Hash)(src); 454 | const mods = await registry.get(srcHash); 455 | const mod = mods[srcHash]; 456 | if (!mod) { throw new ExitStatus(`rholang module not loaded: ${name}`); } 457 | // $FlowFixMe validate mixed -> ModuleInfo 458 | return mod; 459 | } 460 | 461 | 462 | if (require.main === module) { 463 | // Import primitive effects only when invoked as main module. 464 | /* eslint-disable global-require */ 465 | /*global process, setTimeout*/ 466 | main( 467 | process.argv, 468 | { 469 | stdin: process.stdin, 470 | stdout: process.stdout, 471 | readFile: require('fs').readFile, 472 | writeFile: require('fs').writeFile, 473 | join: require('path').join, 474 | }, 475 | { 476 | clock: () => new Date(), 477 | setTimeout, 478 | randomBytes: require('crypto').randomBytes, 479 | }, 480 | { 481 | grpc: require('grpc'), 482 | uuidv4: require('uuid/v4'), 483 | }, 484 | ) 485 | .catch((err) => { 486 | if (err instanceof ExitStatus) { 487 | console.error(err.message); 488 | process.exit(1); 489 | } else { 490 | console.error(err); 491 | console.error(err.stack); 492 | throw err; // bug! 493 | } 494 | }); 495 | } 496 | -------------------------------------------------------------------------------- /rclient/src/pathlib.js: -------------------------------------------------------------------------------- 1 | /** pathlib -- object-oriented file I/O 2 | 3 | Inspired by https://docs.python.org/3/library/pathlib.html 4 | with influence from Emily. @@TODO: cite Emily 5 | 6 | */ 7 | /* global require, exports */ 8 | 9 | // @flow 10 | 11 | /*:: 12 | import fs from 'fs'; 13 | import path from 'path'; 14 | 15 | type ReadAccess = { 16 | readBytes(): Promise; 17 | readText(encoding?: string): Promise; 18 | joinPath(other: string): ReadAccess; 19 | name(): string; 20 | } 21 | 22 | 23 | type WriteAccess = { 24 | writeText(text: string): Promise; 25 | joinPath(other: string): WriteAccess; 26 | readOnly(): ReadAccess; 27 | } 28 | */ 29 | 30 | const { asPromise } = require('./asPromise'); 31 | 32 | exports.fsReadAccess = fsReadAccess; 33 | function fsReadAccess( 34 | path /*: string*/, 35 | readFile /*: typeof fs.readFile */, 36 | join /*: typeof path.join */, 37 | ) /*: ReadAccess*/ { 38 | return Object.freeze({ 39 | name: () => path, 40 | readText: (encoding /*: string*/ = 'utf8') => asPromise(f => readFile(path, encoding, f)), 41 | readBytes: () => asPromise(f => readFile(path, f)), 42 | joinPath: other => fsReadAccess(join(path, other), readFile, join), 43 | }); 44 | } 45 | 46 | exports.fsWriteAccess = fsWriteAccess; 47 | function fsWriteAccess( 48 | path /*: string*/, 49 | writeFile /*: typeof fs.writeFile */, 50 | readFile /*: typeof fs.readFile */, 51 | join /*: typeof path.join */, 52 | ) /*: WriteAccess*/ { 53 | return Object.freeze({ 54 | writeText: text => asPromise(f => writeFile(path, text, f)), 55 | joinPath: other => fsWriteAccess(join(path, other), writeFile, readFile, join), 56 | readOnly: () => fsReadAccess(path, readFile, join), 57 | }); 58 | } 59 | 60 | /*:: 61 | 62 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/StorageArea 63 | export type StorageArea = { 64 | get(key: string): Promise<{ [string]: mixed }>, 65 | set(items: { [string]: mixed }): Promise 66 | } 67 | */ 68 | exports.FileStorage = FileStorage; 69 | function FileStorage(store /*: WriteAccess*/) /*: StorageArea */ { 70 | async function load() { 71 | try { 72 | const txt = await store.readOnly().readText(); 73 | return JSON.parse(txt); 74 | } catch (err) { 75 | if (err.code === 'ENOENT') { 76 | return {}; 77 | } 78 | throw err; 79 | } 80 | } 81 | 82 | async function get(k /*: string*/) /*: Promise<{ [string]: mixed }> */{ 83 | const info = await load(); 84 | return { [k]: info[k] }; 85 | } 86 | 87 | // ISSUE: atomic read / write 88 | async function set(items /*: { [string]: mixed }*/) { 89 | const info = await load(); 90 | Object.entries(items).forEach(([k, v]) => { 91 | info[k] = v; 92 | }); 93 | await store.writeText(JSON.stringify(info, null, 2)); 94 | } 95 | 96 | return Object.freeze({ get, set }); 97 | } 98 | -------------------------------------------------------------------------------- /rclient/src/rfun.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RChain Node API fun 5 | 6 | 7 |

API Fun

8 | 11 |
12 |       ...?
13 |     
14 |

RHOC snapshot projection

15 |
16 |
17 |
18 |
19 |
ETA: ()
20 |
21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /rclient/src/rhoid.js: -------------------------------------------------------------------------------- 1 | /** rhoid -- build rho:id:... URI from public key as in rnode 2 | 3 | This is a port of relevant parts of Registry.scala 27f76eb02ab2 4 | 5 | https://github.com/rchain/rchain/blob/27f76eb02ab2d83bf2bc9cd766157c4723db0854/rholang/src/main/scala/coop/rchain/rholang/interpreter/Registry.scala#L1028-L1047 // eslint-disable-line 6 | 7 | */ 8 | // @flow 9 | /* global require, exports, module, Buffer */ 10 | 11 | const { RholangCrypto, Hex } = require('rchain-api'); 12 | const base32 = require('base32-encoding'); 13 | 14 | const { blake2b256Hash } = RholangCrypto; 15 | 16 | // human-oriented base-32 encoding by Zooko 2002 - 2009 17 | const zbase32 = buf => base32.stringify(buf, 'ybndrfg8ejkmcpqxot1uwisza345h769'); 18 | 19 | const testCase = { 20 | pk: Buffer.from('a2c8007965d9695298679722b6a822b0c36b280ec2ee4ce44454509658345226', 'hex'), 21 | uri: 'rho:id:5x4bpc8y66ek1yiutdkqwm6t46qbaw41f6wwea1eoizbaifx6o5ikb', 22 | }; 23 | 24 | function test(item) { 25 | const uri = pkURI(item.pk); 26 | const hash = blake2b256Hash(item.pk); 27 | console.log({ 28 | pk: item.pk.toString('hex'), 29 | hash: Hex.encode(hash), 30 | uri, 31 | ok: uri === item.uri, 32 | }); 33 | } 34 | 35 | 36 | // ISSUE: rename pkURI to rhoid? 37 | /** 38 | * Build URI from 64 byte public key. 39 | * 40 | * Note: in Registry.scala, this is not a function of its own. 41 | */ 42 | exports.pkURI = pkURI; 43 | function pkURI(pk /*: Buffer*/) { 44 | const hash = blake2b256Hash(pk); 45 | return buildURI(Buffer.from(hash)); 46 | } 47 | 48 | 49 | /** 50 | * Build URI from hash of public key, as in Registry.scala 51 | * 52 | * ported directly from Registry.scala 53 | */ 54 | exports.buildURI = buildURI; 55 | function buildURI(arr /*: Buffer*/) /*: string */{ 56 | const fullKey = Buffer.alloc(34, 0); 57 | arr.copy(fullKey, 0, 0, 32); 58 | const crc = compute(fullKey.slice(0, 32)); 59 | fullKey[32] = crc & 0xff; 60 | fullKey[33] = ((crc & 0xff00) >>> 6) & 0xff; 61 | return `rho:id:${zbase32(fullKey).slice(0, 270 / 5)}`; // 270 bits 62 | } 63 | 64 | 65 | /* Porting the scala involves bit-twiddling, despite Airbnb style. */ 66 | /*eslint no-bitwise: [2, { allow: ["^", "&", "<<", ">>>"] }] */ 67 | 68 | const INIT_REMAINDER = 0; 69 | 70 | /** 71 | * Compute CRC14 from byte sequence. 72 | * 73 | * ported directly from Registry.scala 74 | */ 75 | exports.CRC14 = { compute }; 76 | function compute(b /*: Buffer*/) /*: number */{ 77 | return [...b.values()].reduce(update, INIT_REMAINDER); 78 | } 79 | 80 | function update(rem0, b) { 81 | function loop(i, rem) { 82 | if (i < 8) { 83 | const shiftRem = (rem << 1) & 0xFFFF; 84 | return ((shiftRem & 0x4000) !== 0) 85 | ? loop(i + 1, (shiftRem ^ 0x4805) & 0xFFFF) 86 | : loop(i + 1, shiftRem); 87 | } 88 | return rem; 89 | } 90 | return loop(0, (rem0 ^ (b << 6)) & 0xFFFF); 91 | } 92 | 93 | if (require.main === module) { 94 | test(testCase); 95 | } 96 | -------------------------------------------------------------------------------- /rclient/src/secretStorage.js: -------------------------------------------------------------------------------- 1 | // https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition 2 | // https://github.com/ethereumjs/ethereumjs-wallet/blob/master/src/index.js 3 | // why reimplement? (1) education, (2) ocap discipline (3) static typing 4 | 5 | /* global require, exports, Buffer */ 6 | // @flow 7 | 8 | const scrypt = require('scrypt.js'); // ISSUE: just use crypto.script? 9 | const crypto = require('crypto'); 10 | const assert = require('assert'); 11 | 12 | const { keccak256Hash } = require('rchain-api').RholangCrypto; 13 | 14 | /*:: 15 | 16 | export type Bytes = Buffer | string; 17 | export type EthAddr = Bytes<20>; 18 | export type UUID = string; // ISSUE: opaque? only create from uuid API? 19 | export type PrivateKey = Buffer; // ISSUE: opaque? only create from randomBytes? 20 | 21 | export type SecretStorageV3 = { 22 | version: 3, 23 | id: UUID, 24 | crypto: C & K & { 25 | ciphertext: Bytes<32>, 26 | mac: Bytes<32>, 27 | }, 28 | }; 29 | 30 | export type Cipher = { 31 | cipher: N, 32 | cipherparams: P, 33 | }; 34 | 35 | export type AES128CTR = Cipher<'aes-128-ctr', { iv: Bytes<16> }>; 36 | 37 | export type KDF = { 38 | kdf: N, 39 | kdfparams: P, 40 | }; 41 | 42 | export type SCrypt = KDF<'scrypt', { 43 | dklen: 32, 44 | salt: Bytes<32>, 45 | n: number, 46 | r: number, 47 | p: number, 48 | }>; 49 | */ 50 | 51 | 52 | const testVectorScrypt /*: SecretStorageV3 */ = { 53 | crypto: { 54 | cipher: 'aes-128-ctr', 55 | cipherparams: { iv: '83dbcc02d8ccb40e466191a123791e0e' }, 56 | ciphertext: 'd172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c', 57 | kdf: 'scrypt', 58 | kdfparams: { 59 | dklen: 32, 60 | n: 262144, 61 | p: 8, 62 | r: 1, 63 | salt: 'ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19', 64 | }, 65 | mac: '2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097', 66 | }, 67 | id: '3198bc9c-6672-5ab3-d995-4942343ae5b6', 68 | version: 3, 69 | }; 70 | 71 | const testPk = decrypt(Buffer.from('testpassword'), testVectorScrypt); 72 | assert(testPk.toString('hex') === '7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d'); 73 | 74 | 75 | exports.decrypt = decrypt; 76 | function decrypt( 77 | password /*: Buffer*/, 78 | item /*: SecretStorageV3*/, 79 | ) /*: PrivateKey */ { 80 | assert(item.crypto.kdf === 'scrypt'); 81 | 82 | const { salt, n, r, p, dklen } = item.crypto.kdfparams; 83 | const derivedKey = scrypt(password, toBuf(salt), n, r, p, dklen); 84 | // console.log('Derived key:', derivedKey.toString('hex')); 85 | 86 | const MACBody = Buffer.concat([ 87 | derivedKey.slice(16, 32), 88 | toBuf(item.crypto.ciphertext), 89 | ]); 90 | // console.log('MAC Body', MACBody.toString('hex')); 91 | const MAC = Buffer.from(keccak256Hash(MACBody)); 92 | // console.log('MAC', MAC.toString('hex')); 93 | const diff = MAC.compare(toBuf(item.crypto.mac)); 94 | // console.log('MAC diff?', diff); 95 | if (diff) { 96 | throw new Error('bad MAC (probably bad password)'); 97 | } 98 | 99 | const cipherKey = derivedKey.slice(0, 128 / 8); 100 | assert(item.crypto.cipher === 'aes-128-ctr'); 101 | const decipher = crypto.createDecipheriv( 102 | item.crypto.cipher, cipherKey, Buffer.from(toBuf(item.crypto.cipherparams.iv)), 103 | ); 104 | const privateKey = decipher.update(toBuf(item.crypto.ciphertext)); 105 | return privateKey; 106 | } 107 | 108 | 109 | exports.encrypt = encrypt; 110 | function encrypt( 111 | privateKey /*: PrivateKey */, 112 | password /*: Buffer */, 113 | randomBytes /*: number => Buffer */, 114 | uuidv4 /*: () => UUID */, 115 | ) /*: SecretStorageV3 */ { 116 | const kdf /*: SCrypt */ = { 117 | kdf: 'scrypt', 118 | kdfparams: { 119 | salt: randomBytes(32), 120 | n: 262144, 121 | r: 8, 122 | p: 1, 123 | dklen: 32, 124 | }, 125 | }; 126 | const derivedKey /*: Buffer */ = scrypt(password, ...Object.values(kdf.kdfparams)); 127 | // console.log('Derived key:', derivedKey.toString('hex')); 128 | 129 | const cipherKey = derivedKey.slice(0, 128 / 8); 130 | 131 | const cipher /*: AES128CTR */ = { 132 | cipher: 'aes-128-ctr', 133 | cipherparams: { iv: randomBytes(16) }, 134 | }; 135 | 136 | const encipher = crypto.createCipheriv(cipher.cipher, cipherKey, cipher.cipherparams.iv); 137 | const ciphertext = encipher.update(privateKey); 138 | encipher.final().copy(ciphertext, ciphertext.length); 139 | 140 | const MACBody = Buffer.concat([ 141 | derivedKey.slice(16, 32), 142 | ciphertext, 143 | ]); 144 | // console.log('MAC Body', MACBody.toString('hex')); 145 | const mac = Buffer.from(keccak256Hash(MACBody)).toString('hex'); 146 | // console.log('MAC', MAC.toString('hex')); 147 | 148 | const item = { 149 | version: 3, 150 | id: uuidv4(), 151 | crypto: { 152 | mac, 153 | ciphertext, 154 | ...kdf, 155 | ...cipher, 156 | }, 157 | }; 158 | 159 | // umm... not pretty... 160 | const bytes = item.crypto; 161 | bytes.mac = toHex(item.crypto.mac); 162 | bytes.ciphertext = toHex(item.crypto.ciphertext); 163 | bytes.cipherparams.iv = toHex(bytes.cipherparams.iv); 164 | bytes.kdfparams.salt = toHex(item.crypto.kdfparams.salt); 165 | 166 | return item; 167 | } 168 | 169 | 170 | function toBuf/*:: */(data /*: Bytes*/) /*: Buffer */{ 171 | return typeof data === 'string' ? Buffer.from(data, 'hex') : data; 172 | } 173 | 174 | function toHex/*:: */(data /*: Bytes*/) /*: string */{ 175 | return typeof data === 'string' ? data : data.toString('hex'); 176 | } 177 | -------------------------------------------------------------------------------- /rclient/src/sigTool.js: -------------------------------------------------------------------------------- 1 | /** sigTool -- signing key generation, storage, and usage 2 | 3 | ISSUE: pretty much subsumed by secretStorage. prune nacl dependency too. 4 | 5 | @flow strict 6 | */ 7 | /* global exports, require */ 8 | 9 | const { Hex } = require('rchain-api'); 10 | 11 | const def = Object.freeze; 12 | const b2h = Hex.encode; 13 | const h2b = Hex.decode; 14 | 15 | /*:: 16 | 17 | import nacl from 'tweetnacl'; 18 | import type { StorageArea } from './pathlib'; 19 | import type { HexStr, PublicKey } from 'rchain-api'; 20 | 21 | // SigningKey is the format we use to save the key pair 22 | // with the secret key encrypted. 23 | export type SigningKey = { 24 | label: string, 25 | secretKey: { 26 | // ISSUE: opaque type for hex? 27 | nonce: string, 28 | cipherText: string, 29 | }, 30 | pubKey: HexStr 31 | } 32 | 33 | export interface SigTool { 34 | // Generate and save key. 35 | generate({ label: string, password: string }): Promise, 36 | // Get stored key. 37 | getKey(string): Promise, 38 | // Decrypt private key and use it to sign message. 39 | signMessage(message: Uint8Array, signingKey: SigningKey, password: string): string 40 | } 41 | 42 | */ 43 | 44 | exports.sigTool = sigTool; 45 | function sigTool(local /*: StorageArea */, nacl /*: typeof nacl*/) /*: SigTool */ { 46 | function getKey(label) /*: Promise */{ 47 | return local.get(label).then(items => chkKey(items[label])); 48 | } 49 | 50 | function chkKey(it /*: mixed*/) /*: SigningKey | null */ { 51 | if (it === null) { return null; } 52 | if (typeof it !== 'object') { return null; } 53 | const { secretKey } = it; 54 | if (!secretKey || typeof secretKey !== 'object') { return null; } 55 | const { nonce } = secretKey; 56 | if (typeof nonce !== 'string') { return null; } 57 | const { cipherText } = secretKey; 58 | if (typeof cipherText !== 'string') { return null; } 59 | return { 60 | label: asStr(it.label), 61 | secretKey: { nonce, cipherText }, 62 | pubKey: asStr(it.pubKey), 63 | }; 64 | } 65 | 66 | function generate({ label, password }) { 67 | const signingKey = encryptedKey(nacl.sign.keyPair(), { label, password }); 68 | return local.set({ [label]: signingKey }).then(() => signingKey); 69 | } 70 | 71 | function encryptedKey(keyPair, { label, password }) { 72 | const sk = encryptWithNonce(keyPair.secretKey, passKey(password)); 73 | 74 | return { 75 | label, 76 | getKey, 77 | secretKey: { 78 | nonce: b2h(sk.nonce), 79 | cipherText: b2h(sk.cipherText), 80 | }, 81 | pubKey: b2h(keyPair.publicKey), 82 | }; 83 | } 84 | 85 | /** 86 | * Hash text password to get bytes for secretbox key. 87 | */ 88 | function passKey(password /*: string*/) /*: Uint8Array */{ 89 | return nacl.hash(utf8(password)).slice(0, nacl.secretbox.keyLength); 90 | } 91 | 92 | function encryptWithNonce(message /*: Uint8Array */, key) { 93 | const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); 94 | const cipherText = nacl.secretbox(message, nonce, key); 95 | return { cipherText, nonce }; 96 | } 97 | 98 | function signMessage( 99 | message /*: Uint8Array */, 100 | signingKey /*: SigningKey*/, 101 | password /*: string*/, 102 | ) { 103 | const nonce = h2b(signingKey.secretKey.nonce); 104 | const box = h2b(signingKey.secretKey.cipherText); 105 | const secretKey = nacl.secretbox.open(box, nonce, passKey(password)); 106 | 107 | if (secretKey === null) { 108 | throw new Error('bad password'); 109 | } 110 | 111 | return b2h(nacl.sign.detached(message, secretKey)); 112 | } 113 | 114 | return def({ getKey, generate, signMessage }); 115 | } 116 | 117 | 118 | function utf8(s /*: string*/) /*: Uint8Array*/ { 119 | const byteChars = unescape(encodeURIComponent(s)); 120 | return Uint8Array.from([...byteChars].map(ch => ch.charCodeAt(0))); 121 | } 122 | 123 | 124 | function asStr(x /*: mixed*/) /*: string */ { 125 | if (typeof x !== 'string') { return ''; } 126 | return x; 127 | } 128 | -------------------------------------------------------------------------------- /rclient/src/tool.js: -------------------------------------------------------------------------------- 1 | const harden = x => Object.freeze(x); 2 | 3 | export function makeTool({ fetch }) { 4 | return harden({ 5 | async status() { 6 | const reply = await fetch('/status'); 7 | return reply.json(); 8 | }, 9 | }); 10 | } 11 | 12 | function narrow(base, { fetch }) { 13 | return path => fetch(`${base}${path}`); 14 | } 15 | 16 | function EtherscanAPI(apikey, { fetch }) { 17 | return harden({ 18 | async getblockcountdown(blockno) { 19 | const reply = await fetch(`?module=block&action=getblockcountdown&blockno=${blockno}&apikey=${apikey}`); 20 | const info = await reply.json(); 21 | if (info.status !== '1') { 22 | throw new Error(info.message); 23 | } 24 | return parseFloat(info.result.EstimateTimeInSec); 25 | }, 26 | }); 27 | } 28 | EtherscanAPI.base = 'https://api.etherscan.io/api'; 29 | 30 | export async function main({ alert, fetch, getElementById, now }) { 31 | const byId = getElementById; 32 | function setText(id, text) { 33 | byId(id).textContent = text; 34 | } 35 | function getSelection(id) { 36 | const sel = byId(id); 37 | return sel.options[sel.selectedIndex].value; 38 | } 39 | 40 | const apikey = byId('apikey').value; 41 | const ethInfo = EtherscanAPI(apikey, { fetch: narrow(EtherscanAPI.base, { fetch }) }); 42 | byId('project').addEventListener('click', async () => { 43 | const countdownBlock = byId('CountdownBlock').value; 44 | let durSec; 45 | try { 46 | durSec = await ethInfo.getblockcountdown(countdownBlock); 47 | } catch (err) { 48 | alert(err.message); 49 | return; 50 | } 51 | const eta = new Date(now() + durSec * 1000); 52 | byId('ETA').textContent = eta.toISOString(); 53 | const d = { 54 | hh: (durSec / 60 / 60) | 0, 55 | mm: ((durSec / 60) % 60) | 0, 56 | ss: (durSec | 0) % 60, 57 | }; 58 | byId('delta').textContent = `${d.hh}:${d.mm}:${d.ss}`; 59 | }); 60 | const addr = getSelection('nodeAddress'); 61 | const tool = makeTool({ fetch: narrow(addr, { fetch }) }); 62 | const info = await tool.status(); 63 | setText('status', JSON.stringify(info)); 64 | } 65 | -------------------------------------------------------------------------------- /rclient/src/tools.rho: -------------------------------------------------------------------------------- 1 | /** 2 | * tools - stuff n junk 3 | */ 4 | new 5 | Tools, 6 | RevVaultCh, 7 | // export, // ISSUE: rholang extension 8 | export(`export:`), // ISSUE: rholang extension 9 | trace(`rho:io:stderr`), 10 | lookup(`rho:registry:lookup`), 11 | insertArbitrary(`rho:registry:insertArbitrary`), 12 | insertSigned(`rho:registry:insertSigned:ed25519`) 13 | in { 14 | export!(*Tools) | trace!("rclient: Tools OK") | 15 | 16 | lookup!(`rho:id:1o93uitkrjfubh43jt19owanuezhntag5wh74c6ur5feuotpi73q8z`, *RevVaultCh) | 17 | 18 | for( 19 | @(_, *RevVault) <- RevVaultCh 20 | ) { 21 | 22 | contract Tools(@"genVault", 23 | @{revAddress /\ String}, 24 | @{amount /\ Int}, 25 | return 26 | ) = { 27 | new vaultCh, balanceCh in { 28 | RevVault!("findOrCreateGenesisVault", revAddress, amount, *vaultCh) | 29 | for(@(true, vault) <- vaultCh) { 30 | trace!({"tag": "rclient", "new vault": vault, "amount": amount}) | 31 | 32 | @vault!("balance", *balanceCh) | for (@balance <- balanceCh) { 33 | trace!({"tag": "rclient", "new vault balance": balance}) | 34 | return!((true, amount, balance)) 35 | } 36 | } 37 | } 38 | } | 39 | 40 | contract Tools(@"balance", 41 | @{revAddress /\ String}, 42 | return 43 | ) = { 44 | new vaultCh, balanceCh in { 45 | RevVault!("findOrCreate", revAddress, *vaultCh) | for (@(true, vault) <- vaultCh) { 46 | trace!("rclient: Obtained vault, checking balance") | 47 | 48 | @vault!("balance", *balanceCh) | for (@balance <- balanceCh) { 49 | trace!({"tag": "rclient", "balance": balance}) | 50 | return!(balance) 51 | } 52 | } 53 | } 54 | } | 55 | 56 | contract Tools(@"transfer", 57 | @{from /\ String}, // RevAddress 58 | @{to /\ String}, // RevAddress 59 | @{amount /\ Int}, 60 | return 61 | ) = { 62 | trace!({"rclient": "transfer", "amount": amount, "from": from, "to": to }) | 63 | 64 | new vaultCh, revVaultkeyCh in { 65 | RevVault!("findOrCreate", from, *vaultCh) | 66 | RevVault!("deployerAuthKey", *revVaultkeyCh) | 67 | for (@(true, vault) <- vaultCh; key <- revVaultkeyCh) { 68 | 69 | new resultCh in { 70 | @vault!("transfer", to, amount, *key, *resultCh) | 71 | for (@result <- resultCh) { 72 | trace!({"rclient": "txfer result", "amount": amount, "to": to, "result": result}) | 73 | return!(result) 74 | } 75 | } 76 | } | 77 | for (@(false, why) <- vaultCh; _ <- revVaultkeyCh) { 78 | trace!({"rclient": "findOrCreate failed", "why": why}) | 79 | return!((false, why)) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/assets.js: -------------------------------------------------------------------------------- 1 | /* assets - treat text files as link-time artifacts. 2 | */ 3 | /* global require, exports */ 4 | // @flow 5 | 6 | const { readFileSync } = require('fs'); 7 | 8 | exports.link = link; 9 | /** 10 | * link a string asset 11 | * 12 | * @private 13 | * 14 | * With respect to ocap discipline, we regard this as part of the 15 | * module loading infrastructure rather than a run-time operation. 16 | * 17 | * An alternative implementation would be a pre-processor 18 | * that in-lines the contents of the linked data as constants. 19 | */ 20 | function link(name /*: string*/) /*: string*/ { 21 | return readFileSync(require.resolve(name), 'utf8'); 22 | } 23 | -------------------------------------------------------------------------------- /src/codec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import base58 from 'bs58'; 3 | 4 | const { freeze } = Object; 5 | 6 | export const Base16 = freeze({ 7 | /** 8 | * Encode bytes to base 16 string. 9 | * 10 | * @param {Uint8Array | number[]} bytes 11 | * @returns { string } 12 | */ 13 | encode(bytes) { 14 | return ( 15 | Array.from(bytes) 16 | // eslint-disable-next-line no-bitwise 17 | .map((x) => (x & 0xff).toString(16).padStart(2, '0')) 18 | .join('') 19 | ); 20 | }, 21 | 22 | /** 23 | * Decode base 16 string to bytes. 24 | * 25 | * @param {string} hexStr 26 | */ 27 | decode: (hexStr) => { 28 | const removed0x = hexStr.replace(/^0x/, ''); 29 | const byte2hex = ([arr, bhi], x) => 30 | bhi ? [[...arr, parseInt(`${bhi}${x}`, 16)]] : [arr, x]; 31 | const [resArr] = Array.from(removed0x).reduce(byte2hex, [[]]); 32 | return Uint8Array.from(resArr); 33 | }, 34 | 35 | /** 36 | * Encode bytes as hex string 37 | * 38 | * ISSUE: uses Buffer API. Require this for browsers? 39 | * @param {Uint8Array | Buffer} bytes 40 | * @returns { string } 41 | */ 42 | encodeBuf(bytes) { 43 | return Buffer.from(bytes).toString('hex'); 44 | }, 45 | 46 | /** 47 | * Decode hex string to bytes 48 | * @param {string} hex in hex (base16) 49 | * @returns { Buffer } 50 | */ 51 | decodeBuf(hex) { 52 | return Buffer.from(hex, 'hex'); 53 | }, 54 | }); 55 | 56 | export const Base58 = freeze({ 57 | /** 58 | * Encode base 16 string to base 58. 59 | * 60 | * @param {string} hexStr 61 | * @returns {string} 62 | */ 63 | encode: (hexStr) => { 64 | const bytes = Base16.decode(hexStr); 65 | return base58.encode(bytes); 66 | }, 67 | 68 | /** 69 | * Decode base 58 string (handle errors). 70 | * 71 | * @param {string} str 72 | * @returns {Uint8Array | undefined} 73 | */ 74 | decodeSafe: (str) => { 75 | try { 76 | return base58.decode(str); 77 | } catch (_) { 78 | return undefined; 79 | } 80 | }, 81 | }); 82 | 83 | export const Ascii = freeze({ 84 | /** 85 | * Decode ASCII string to bytes. 86 | * 87 | * @param {string} str 88 | */ 89 | decode: (str = '') => Array.from(str).map((x) => `${x}`.charCodeAt(0)), 90 | }); 91 | -------------------------------------------------------------------------------- /src/curl.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // @jessie-check 3 | 4 | import { makePromise } from 'jessie.js'; 5 | 6 | /** 7 | * @param {string} url 8 | * @param {{ http: any }} powers 9 | * @returns {Promise} 10 | */ 11 | export function curl(url, { http }) { 12 | return makePromise((resolve, reject) => { 13 | const req = http.get(url, response => { 14 | let str = ''; 15 | // console.log('Response is ' + response.statusCode); 16 | response.on('data', chunk => { 17 | str += chunk; 18 | }); 19 | response.on('end', () => resolve(str)); 20 | }); 21 | req.end(); 22 | req.on('error', reject); 23 | }); 24 | } 25 | 26 | /** 27 | * @returns { typeof fetch } 28 | */ 29 | export function nodeFetch({ http }) { 30 | /** @type { typeof fetch } */ 31 | function fetch(url, options = {}) { 32 | return makePromise((resolve, reject) => { 33 | const { method = 'GET', body } = options; 34 | if (typeof url !== 'string') { throw Error('not implemented'); } 35 | const req = http.request(url, { method }, res => { 36 | res.setEncoding('utf8'); 37 | let content = ''; 38 | res.on('data', data => { content += data; }); 39 | 40 | res.on('end', () => { 41 | /** @type any */ 42 | const response = { 43 | ok: res.statusCode && res.statusCode >= 200 && res.statusCode < 300, 44 | status: res.statusCode, 45 | statusText: res.statusMessage, 46 | text: () => content, 47 | json: () => JSON.parse(content), 48 | }; 49 | resolve(response); 50 | }); 51 | }); 52 | if (body !== undefined) { 53 | req.write(body); 54 | } 55 | req.on('error', reject); 56 | req.end(); 57 | }); 58 | } 59 | 60 | return fetch; 61 | } 62 | -------------------------------------------------------------------------------- /src/deploySig.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import blake from 'blakejs'; 4 | import elliptic from 'elliptic'; 5 | import jspb from 'google-protobuf'; 6 | import * as ethUtil from 'ethereumjs-util'; 7 | 8 | import { Base16 } from './codec.js'; 9 | import { MetaMaskAccount } from './ethProvider.js'; 10 | 11 | // eslint-disable-next-line new-cap 12 | const secp256k1 = new elliptic.ec('secp256k1'); 13 | 14 | /** 15 | * @param {string} keyHex 16 | * @param {DeployData} info 17 | * @typedef {import('./rnode-openapi-schema').DeployData} DeployData 18 | */ 19 | export function signPrep(keyHex, info) { 20 | const key = secp256k1.keyFromPrivate(keyHex); 21 | // eslint-disable-next-line no-use-before-define 22 | const deploySerialized = serialize(info); 23 | const hashed = blake.blake2bHex(deploySerialized, undefined, 32); 24 | const deployer = Uint8Array.from(key.getPublic('array')); 25 | const sigArray = key.sign(hashed, { canonical: true }).toDER('array'); 26 | const signature = Uint8Array.from(sigArray); 27 | const signedDeploy = { ...info, signature, deployer }; 28 | return { 29 | hashed, 30 | deployer, 31 | deploySerialized, 32 | key, 33 | sigArray, 34 | signature, 35 | signedDeploy, 36 | }; 37 | } 38 | 39 | /** 40 | * @param {DeployData} info 41 | */ 42 | export function serialize(info) { 43 | const { term, timestamp, phloPrice, phloLimit, validAfterBlockNumber } = info; 44 | 45 | // Serialize payload with protobuf 46 | const writer = new jspb.BinaryWriter(); 47 | // Write fields (protobuf doesn't serialize default values) 48 | // TODO: test! 49 | const writeString = (order, val) => 50 | val !== '' && writer.writeString(order, val); 51 | const writeInt64 = (order, val) => val !== 0 && writer.writeInt64(order, val); 52 | 53 | writeString(2, term); 54 | writeInt64(3, timestamp); 55 | writeInt64(7, phloPrice); 56 | writeInt64(8, phloLimit); 57 | writeInt64(10, validAfterBlockNumber); 58 | return writer.getResultBuffer(); 59 | } 60 | 61 | /** 62 | * @param {string} keyHex 63 | * @param {DeployData} info 64 | * @returns { DeployRequest } 65 | * @typedef {import('./rnode-openapi-schema').DeployRequest} DeployRequest 66 | */ 67 | export function sign(keyHex, info) { 68 | const { term, timestamp, phloPrice, phloLimit, validAfterBlockNumber } = info; 69 | const { deployer, signature } = signPrep(keyHex, info); 70 | 71 | return { 72 | data: { 73 | term, 74 | timestamp, 75 | phloPrice, 76 | phloLimit, 77 | validAfterBlockNumber, 78 | }, 79 | sigAlgorithm: 'secp256k1', 80 | signature: Base16.encode(signature), 81 | deployer: Base16.encode(deployer), 82 | }; 83 | } 84 | 85 | /** 86 | * @param {Iterable} data 87 | * @param {string} sigHex 88 | * @returns { string } public key in hex 89 | */ 90 | export function recoverPublicKeyEth(data, sigHex) { 91 | // Ethereum lib to recover public key from massage and signature 92 | const hashed = ethUtil.hashPersonalMessage(ethUtil.toBuffer([...data])); 93 | const sigBytes = ethUtil.toBuffer(sigHex); 94 | // @ts-ignore fromRpcSig seems ok with a buffer as well as string 95 | const { v, r, s } = ethUtil.fromRpcSig(sigBytes); 96 | // Public key without prefix 97 | const pubkeyRecover = ethUtil.ecrecover(hashed, v, r, s); 98 | 99 | return Base16.encode([4, ...pubkeyRecover]); 100 | } 101 | 102 | /** 103 | * Serialize and sign with Metamask extension 104 | * - this will open a popup for user to confirm/review 105 | * 106 | * @param {DeployData} deployData 107 | * @param {MetaMaskProvider} ethereum 108 | * @returns {Promise} 109 | * 110 | * @typedef { import('./ethProvider').MetaMaskProvider } MetaMaskProvider 111 | */ 112 | export async function signMetaMask(deployData, ethereum) { 113 | const data = serialize(deployData); 114 | const acct = MetaMaskAccount(ethereum); 115 | const ethAddr = await acct.ethereumAddress(); 116 | const sigHex = await acct.ethereumSign(data, ethAddr); 117 | // Extract public key from signed message and signature 118 | const pubKeyHex = recoverPublicKeyEth(data, sigHex); 119 | // Create deploy object for signature verification 120 | return { 121 | data: deployData, 122 | signature: sigHex.replace(/^0x/, ''), 123 | deployer: pubKeyHex, 124 | sigAlgorithm: 'secp256k1:eth', 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /src/ethProvider.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // adapted from https://github.com/tgrospic/rnode-client-js/blob/e4d97ee436a9a35ed2fe7b6c3307d03e4e2e9612/src/eth/eth-wrapper.js 3 | 4 | const { freeze } = Object; 5 | 6 | /** 7 | * "To notify sites of asynchronous injection, 8 | * MetaMask dispatches the ethereum#initialized event 9 | * on `window` immediately after the provider 10 | * has been set as `window.ethereum`." 11 | * -- https://github.com/MetaMask/detect-provider 12 | */ 13 | export const MetaMask = { 14 | initialized: 'ethereum#initialized', 15 | }; 16 | 17 | /** 18 | * Metamask wrapper for Ethereum provider 19 | * https://metamask.github.io/metamask-docs/guide/ethereum-provider.html#methods-new-api 20 | * 21 | * @param {{ window: { 22 | * ethereum?: MetaMaskProvider, 23 | * addEventListener: typeof window.addEventListener, 24 | * removeEventListener: typeof window.removeEventListener, 25 | * }}} io 26 | * 27 | * @typedef {{ 28 | * readonly isMetaMask: boolean, 29 | * autoRefreshOnNetworkChange: boolean, 30 | * request: (args: RequestArguments) => Promise 31 | * }} MetaMaskProvider 32 | * 33 | * EIP-1193: Ethereum Provider JavaScript API 34 | * https://eips.ethereum.org/EIPS/eip-1193 35 | * @typedef {{ 36 | * readonly method: string; 37 | * readonly params?: readonly unknown[] | object; 38 | * }} RequestArguments 39 | */ 40 | export function getEthProvider({ window }) { 41 | return new Promise((resolve) => { 42 | if (window.ethereum) { 43 | resolve(window.ethereum); 44 | } 45 | 46 | // eslint-disable-next-line no-use-before-define 47 | window.addEventListener(MetaMask.initialized, handle, { once: true }); 48 | 49 | function handle() { 50 | window.removeEventListener(MetaMask.initialized, handle); 51 | const { ethereum } = window; 52 | if (!ethereum) 53 | throw new Error( 54 | `${MetaMask.initialized} event fired with window.ethereum not defined.`, 55 | ); 56 | resolve(ethereum); 57 | } 58 | }); 59 | } 60 | 61 | /** 62 | * @param {MetaMaskProvider} ethereum 63 | */ 64 | export function MetaMaskAccount(ethereum) { 65 | // https://docs.metamask.io/guide/ethereum-provider.html#properties 66 | ethereum.autoRefreshOnNetworkChange = false; 67 | 68 | return freeze({ 69 | /** 70 | * Request an address selected in Metamask 71 | * - the first request will ask the user for permission 72 | * @returns { Promise } ETH address in hex format 73 | */ 74 | async ethereumAddress() { 75 | const accounts = await ethereum.request({ 76 | method: 'eth_requestAccounts', 77 | }); 78 | 79 | if (!Array.isArray(accounts)) 80 | throw Error( 81 | `Ethereum RPC response is not a list of accounts (${accounts}).`, 82 | ); 83 | 84 | return accounts[0]; 85 | }, 86 | 87 | /** 88 | * Ethereum personal signature 89 | * https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign 90 | * @param {Iterable} bytes 91 | * @param {string} ethAddr 92 | * @returns { Promise } signature in hex format 93 | */ 94 | async ethereumSign(bytes, ethAddr) { 95 | let data = `0x${Buffer.from([...bytes], 'utf8').toString('hex')}`; 96 | const sig = await ethereum.request({ 97 | method: 'personal_sign', 98 | params: [data, ethAddr], 99 | }); 100 | if (typeof sig !== 'string') throw new TypeError(typeof sig); 101 | return sig; 102 | }, 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /src/proxy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | /* eslint-disable no-use-before-define */ 3 | // @ts-check 4 | import { sign as signDeploy } from './deploySig'; 5 | import { RhoExpr } from './rho-expr'; 6 | 7 | const { freeze } = Object; 8 | 9 | /** 10 | * Find deploy, fetch its block, and extract relevant DeployInfo 11 | * 12 | * @param {Observer} observer 13 | * @param {string} deployId 14 | * @returns { Promise } null if not (yet) available 15 | * @throws { Error } in case of deploy execution error 16 | * 17 | * @typedef { import("./rnode-openapi-schema").DeployInfo } DeployInfo 18 | */ 19 | export async function checkForDeployInfo(observer, deployId) { 20 | // Request a block with the deploy 21 | const block = await observer.findDeploy(deployId).catch((ex) => { 22 | // Handle response code 400 / deploy not found 23 | if (ex.status !== 400) throw ex; 24 | }); 25 | if (!block) return null; 26 | const { deploys } = await observer.getBlock(block.blockHash); 27 | const deploy = deploys.find(({ sig }) => sig === deployId); 28 | if (!deploy) { 29 | // This should not be possible if block is returned 30 | throw Error(`Deploy is not found in the block (${block.blockHash}).`); 31 | } 32 | return deploy; 33 | } 34 | 35 | /** 36 | * Get result of deploy 37 | * @param {Observer} observer 38 | * @param {DeployInfo} deploy 39 | * @returns { Promise } 40 | * @throws { Error } in case of execution error or missing data 41 | * 42 | * @typedef { import("./rnode-openapi-schema").RhoExprWithBlock } RhoExprWithBlock 43 | */ 44 | export async function listenAtDeployId(observer, deploy) { 45 | // Check deploy errors 46 | const { errored, systemDeployError } = deploy; 47 | if (errored) { 48 | throw Error(`Deploy error when executing Rholang code.`); 49 | } else if (systemDeployError) { 50 | throw Error(`${systemDeployError} (system error).`); 51 | } 52 | 53 | const target = { depth: 1, name: { UnforgDeploy: { data: deploy.sig } } }; 54 | 55 | // Request for data at deploy signature (deployId) 56 | const { exprs } = await observer.listenForDataAtName(target); 57 | // Return data with cost (assumes data in one block) 58 | if (!exprs.length) throw new Error('no data at deployId'); 59 | // TODO: return all exprs; let caller pick 1st 60 | return exprs[0]; 61 | } 62 | 63 | /** 64 | * 65 | * @param {string} pkHex 66 | * @param { Observer } observer 67 | * @param {{ 68 | * setTimeout: typeof setTimeout, 69 | * clock: () => Promise, 70 | * period?: number 71 | * }} sched 72 | * @returns { Account } 73 | * 74 | * @typedef {{ 75 | * sign: (term: string) => Promise, 76 | * polling: () => Promise, // throws to abort 77 | * }} Account 78 | */ 79 | export function makeAccount( 80 | pkHex, 81 | observer, 82 | { setTimeout, clock, period = 7500 }, 83 | { phloPrice = 1, phloLimit = 250000 }, 84 | ) { 85 | const polling = () => 86 | new Promise((resolve) => { 87 | setTimeout(resolve, period); 88 | }); 89 | 90 | return freeze({ 91 | polling, 92 | 93 | /** 94 | * @param {string} term 95 | * @returns { Promise } 96 | */ 97 | async sign(term) { 98 | const [timestamp, [recent]] = await Promise.all([ 99 | clock(), 100 | observer.getBlocks(1), 101 | ]); 102 | return signDeploy(pkHex, { 103 | term, 104 | phloPrice, 105 | phloLimit, 106 | timestamp, 107 | validAfterBlockNumber: recent.blockNumber, 108 | }); 109 | }, 110 | }); 111 | } 112 | 113 | /** 114 | * Sign, deploy, get block with deploy 115 | * @param {string} term 116 | * @param {Validator} validator 117 | * @param {Observer} observer 118 | * @param {Account} account 119 | * @returns {Promise} 120 | */ 121 | export async function startTerm( 122 | /** @type {string} */ term, 123 | validator, 124 | observer, 125 | account, 126 | ) { 127 | const signed = await account.sign(term); 128 | console.log('startTerm', { deployRequest: signed }); 129 | console.log(term); 130 | 131 | const step1 = await validator.deploy(signed); 132 | if (!step1.startsWith('Success')) throw new Error(step1); 133 | 134 | for (;;) { 135 | const deploy = await checkForDeployInfo(observer, signed.signature); 136 | if (deploy) { 137 | return deploy; 138 | } 139 | await account.polling(); 140 | } 141 | } 142 | 143 | /** 144 | * @param {Validator} validator 145 | * @param {Observer} observer 146 | * @param {{ 147 | * sign: (term: string) => Promise, 148 | * polling: () => Promise, // throws to abort 149 | * }} account 150 | * 151 | * TODO: marshalling / unmarshalling of object references 152 | * 153 | * @typedef { null | boolean | number | string } Scalar 154 | * @typedef {Scalar[] | {[k: string]: Scalar}} Complex 155 | * 156 | * @typedef {import('./rnode').Validator} Validator 157 | * @typedef {import('./rnode').Observer} Observer 158 | * @typedef {import('./rnode').DeployRequest} DeployRequest 159 | */ 160 | export function makeConnection(validator, observer, account) { 161 | const { stringify: lit } = JSON; 162 | const spread = (items) => lit(items).slice(1, -1); // strip [] 163 | 164 | const start = (/** @type string */ term) => 165 | startTerm(term, validator, observer, account); 166 | 167 | /** 168 | * @param {Scalar | Complex} tag 169 | * @param {string | Symbol | number} method 170 | * @param {(Scalar | Complex)[]} args 171 | */ 172 | async function sendMessage(tag, method, args) { 173 | const term = `new return(\`rho:rchain:deployId\`), deployerId(\`rho:rchain:deployerId\`) in { 174 | match {[*deployerId, ${lit(tag)}]} { 175 | {*target} => target!(${spread([method, ...args])}, *return) 176 | } 177 | }`; 178 | const deploy = await start(term); 179 | const { expr } = await listenAtDeployId(observer, deploy); 180 | return RhoExpr.parse(expr); 181 | } 182 | 183 | function proxy(/** @type {Scalar | Complex} */ tag) { 184 | return new Proxy(freeze({}), { 185 | get: (obj, method) => { 186 | console.log('proxy get', { obj, tag, method }); 187 | if (method === 'then') return null; // I'm not a Thenable. 188 | return (...args) => { 189 | console.log('proxy sendMessage', { tag, method, args }); 190 | return sendMessage(tag, method, args); 191 | }; 192 | }, 193 | }); 194 | } 195 | 196 | return freeze({ 197 | /** 198 | * @param {Scalar | Complex} tag 199 | * @param {string} targetProc proc with defining use of the name `target` 200 | */ 201 | async spawn(tag, targetProc) { 202 | const term = `new deployerId(\`rho:rchain:deployerId\`) in { 203 | match {[*deployerId, ${lit(tag)}]} { 204 | {*target} => { ${targetProc} } 205 | } 206 | }`; 207 | await start(term); 208 | return proxy(tag); 209 | }, 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /src/rev-address.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import sha3 from 'js-sha3'; 3 | import blake from 'blakejs'; 4 | import elliptic from 'elliptic'; 5 | import { Base16, Base58 } from './codec'; 6 | 7 | const { keccak256 } = sha3; 8 | const { ec } = elliptic; 9 | 10 | // eslint-disable-next-line new-cap 11 | const secp256k1 = new ec('secp256k1'); 12 | 13 | // Algorithm to generate ETH and REV address is taken from RNode source 14 | // https://github.com/rchain/rchain/blob/bf7a30e1/rholang/src/main/scala/coop/rchain/rholang/interpreter/util/AddressTools.scala#L47 15 | 16 | // Prefix as defined in https://github.com/rchain/rchain/blob/c6721a6/rholang/src/main/scala/coop/rchain/rholang/interpreter/util/RevAddress.scala#L13 17 | const prefix = { coinId: '000000', version: '00' }; 18 | 19 | /** 20 | * @typedef {Object} RevAccount - Represents different formats of REV address 21 | * @property {string=} privKey 22 | * @property {string=} pubKey 23 | * @property {string=} ethAddr 24 | * @property {string} revAddr 25 | */ 26 | 27 | /** 28 | * Get REV address from ETH address. 29 | * 30 | * @param {string} ethAddrRaw 31 | * @returns {string | null} 32 | */ 33 | export const getAddrFromEth = (ethAddrRaw) => { 34 | const ethAddr = ethAddrRaw.replace(/^0x/, ''); 35 | if (!ethAddr || ethAddr.length !== 40) return null; 36 | 37 | // Hash ETH address 38 | const ethAddrBytes = Base16.decode(ethAddr); 39 | const ethHash = keccak256(ethAddrBytes); 40 | 41 | // Add prefix with hash and calculate checksum (blake2b-256 hash) 42 | const payload = `${prefix.coinId}${prefix.version}${ethHash}`; 43 | const payloadBytes = Base16.decode(payload); 44 | const checksum = blake.blake2bHex(payloadBytes, undefined, 32).slice(0, 8); 45 | 46 | // Return REV address 47 | return Base58.encode(`${payload}${checksum}`); 48 | }; 49 | 50 | /** 51 | * Get REV address (with ETH address) from public key. 52 | * 53 | * @param {string} publicKeyRaw 54 | */ 55 | export const getAddrFromPublicKey = (publicKeyRaw) => { 56 | const publicKey = publicKeyRaw.replace(/^0x/, ''); 57 | if (!publicKey || publicKey.length !== 130) return null; 58 | 59 | // Public key bytes from hex string 60 | const pubKeyBytes = Base16.decode(publicKey); 61 | // Remove one byte from pk bytes and hash 62 | const pkHash = keccak256(pubKeyBytes.slice(1)); 63 | // Take last 40 chars from hashed pk (ETH address) 64 | const ethAddr = pkHash.slice(-40); 65 | 66 | const revAddr = getAddrFromEth(ethAddr); 67 | if (!revAddr) throw new TypeError('assert'); 68 | 69 | // Return both REV and ETH address 70 | return { revAddr, ethAddr }; 71 | }; 72 | 73 | /** 74 | * Get REV address (with ETH address and public key) from private key. 75 | * 76 | * @param {string} privateKeyRaw 77 | */ 78 | export const getAddrFromPrivateKey = (privateKeyRaw) => { 79 | const privateKey = privateKeyRaw.replace(/^0x/, ''); 80 | if (!privateKey || privateKey.length !== 64) return null; 81 | 82 | // Generate REV address from private key 83 | const key = secp256k1.keyFromPrivate(privateKey); 84 | const pubKey = key.getPublic('hex'); 85 | const addr = getAddrFromPublicKey(pubKey); 86 | if (!addr) throw new TypeError('assert'); 87 | 88 | // Return public key, REV and ETH address 89 | return { pubKey, ...addr }; 90 | }; 91 | 92 | /** 93 | * Verify REV address 94 | * @param {string} revAddr 95 | */ 96 | export const verifyRevAddr = (revAddr) => { 97 | const revBytes = Base58.decodeSafe(revAddr); 98 | if (!revBytes) return undefined; 99 | 100 | // Extract payload and checksum 101 | const revHex = Base16.encode(revBytes); 102 | const payload = revHex.slice(0, -8); // without checksum 103 | const checksum = revHex.slice(-8); // without payload 104 | // Calculate checksum 105 | const payloadBytes = Base16.decode(payload); 106 | const checksumCalc = blake 107 | .blake2bHex(payloadBytes, undefined, 32) 108 | .slice(0, 8); 109 | 110 | return checksum === checksumCalc; 111 | }; 112 | 113 | /** 114 | * Creates REV address from different formats 115 | * (private key -> public key -> ETH address -> REV address) 116 | * 117 | * @param {string} text 118 | * @returns {RevAccount=} 119 | */ 120 | export const createRevAccount = (text) => { 121 | const val = text.replace(/^0x/, '').trim(); 122 | 123 | // Account from private key, public key, ETH or REV address 124 | const fromPriv = getAddrFromPrivateKey(val); 125 | const fromPub = getAddrFromPublicKey(val); 126 | const fromEth = getAddrFromEth(val); 127 | const isRev = verifyRevAddr(val); 128 | 129 | if (isRev) { 130 | return { revAddr: text }; 131 | } else if (fromPriv) { 132 | return { privKey: val, ...fromPriv }; 133 | } else if (fromPub) { 134 | return { pubKey: val, ...fromPub }; 135 | } else if (fromEth) { 136 | return { privKey: '', pubKey: '', ethAddr: val, revAddr: fromEth }; 137 | } 138 | throw new RangeError(); 139 | }; 140 | -------------------------------------------------------------------------------- /src/rho-expr.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { freeze, entries, fromEntries } = Object; 3 | 4 | export const RhoExpr = freeze({ 5 | /** 6 | * @param { RhoExpr } expr 7 | */ 8 | parse(expr) { 9 | const recur = RhoExpr.parse; 10 | // @ts-ignore 11 | // eslint-disable-next-line no-unused-vars 12 | const [[type, { data }], ..._] = entries(expr); 13 | switch (type) { 14 | case 'ExprBool': 15 | case 'ExprInt': 16 | case 'ExprString': 17 | case 'ExprBytes': 18 | case 'ExprUri': 19 | case 'ExprUnforg': 20 | return data; 21 | case 'ExprList': 22 | case 'ExprTuple': 23 | return data.map(recur); 24 | case 'ExprMap': 25 | return fromEntries(entries(data).map(([k, v]) => [k, recur(v)])); 26 | case 'ExprPar': 27 | return { ExprPar: data.map(recur) }; 28 | default: 29 | throw new TypeError(type); 30 | } 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/rhopm.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { nodeFetch } from './curl'; 4 | import { RNode } from './rnode'; 5 | import { startTerm, listenAtDeployId } from './proxy'; 6 | 7 | // @ts-ignore 8 | const { keys, freeze, fromEntries } = Object; 9 | 10 | // TODO: vary rhoDir for local, testnet, mainnet 11 | export const rhoDir = 'rho_modules'; 12 | export const rhoInfoPath = (src) => 13 | `${rhoDir}/${src.replace(/\.rho$/, '.json')}`; 14 | export const importPattern = /match\s*\("import",\s*"(?[^"]+)",\s*`(?rho:id:[^`]*)`\)/g; 15 | 16 | function log(...args) { 17 | if ( 18 | typeof process !== 'undefined' && 19 | typeof process.env !== 'undefined' && 20 | !process.env.LOG_RHOPM_QUIET 21 | ) { 22 | console.log(...args); 23 | } 24 | } 25 | 26 | /** 27 | * @param {{src: string, dataForDeploy: RhoExprWithBlock }} info 28 | * WARNING: we assume expr is URI 29 | * 30 | * @typedef { import('./rnode-openapi-schema').RhoExpr } RhoExpr 31 | */ 32 | export function depEntry(info) { 33 | const { 34 | src: dep, 35 | dataForDeploy: { 36 | expr: { 37 | // @ts-ignore 38 | ExprUri: { data: uri }, 39 | }, 40 | }, 41 | } = info; 42 | return [dep, uri]; 43 | } 44 | 45 | /** 46 | * @param {string} src 47 | * @param {string} term 48 | * @param {{[specifier: string]: string}} uriByDep 49 | */ 50 | export function fixupImports(src, term, uriByDep) { 51 | const each = (_match, specifier, _uri) => { 52 | const dep = specifier.replace(/^\.\//, ''); // TODO: more path resolution? 53 | const uri = uriByDep[dep]; 54 | if (!uri) { 55 | throw new Error( 56 | `failed to satisfy ${src} -> ${dep} dependency among ${keys(uriByDep)}`, 57 | ); 58 | } 59 | return `match ("import", "${specifier}", \`${uri}\`)`; 60 | }; 61 | return term.replace(importPattern, each); 62 | } 63 | 64 | /** 65 | * WARNING: we assume format of depTargets files are correct: 66 | * {src: string, dataForDeploy: RhoExprWithBlock } 67 | * 68 | * @param {string} src 69 | * @param {string[]} depTargets 70 | * @param {{ readFile: typeof import('fs').promises.readFile }} io 71 | * @returns { Promise } 72 | */ 73 | export async function resolveDeps(src, depTargets, { readFile }) { 74 | log('resolve', { src }); 75 | const termRaw = await readFile(src, 'utf8'); 76 | log('resolve', { src, depTargets }); 77 | const reading = depTargets.map((fn) => readFile(fn, 'utf8')); 78 | const info = (await Promise.all(reading)).map((txt) => JSON.parse(txt)); 79 | const byDep = fromEntries(info.map(depEntry)); 80 | log({ src, byDep }); 81 | return fixupImports(src, termRaw, byDep); 82 | } 83 | 84 | /** 85 | * @template T 86 | * @param {undefined | T} x 87 | * @returns {T} 88 | */ 89 | function notNull(x) { 90 | if (!x) { 91 | throw new Error('null!'); 92 | } 93 | return x; 94 | } 95 | 96 | /** 97 | * @param {string} term 98 | * @returns { string[] } 99 | */ 100 | export function findImports(term) { 101 | return [...term.matchAll(importPattern)].map( 102 | (m) => notNull(m.groups).specifier, 103 | ); 104 | } 105 | 106 | /** @type { (n: number) => (s: string) => string } */ 107 | const abbr = (n) => (s) => 108 | (s.length > n ? `${s.slice(0, n)}...` : s).replace(/\s+/g, ' '); 109 | 110 | /** 111 | * @param {{ 112 | * validator: Validator, 113 | * observer: Observer, 114 | * startProposing: () => void, 115 | * stopProposing: () => void, 116 | * }} shard 117 | * @param {Account} account 118 | * @param {{ readFile: typeof import('fs').promises.readFile }} io 119 | * 120 | * @typedef { import('./rnode').Validator } Validator 121 | * @typedef { import('./rnode').Observer } Observer 122 | * @typedef { import('./proxy').Account } Account 123 | * @typedef { import('./rnode-openapi-schema').DeployRequest } DeployRequest 124 | * @typedef { import('./rnode-openapi-schema').RhoExprWithBlock } RhoExprWithBlock 125 | */ 126 | export function PkgManager(shard, account, { readFile }) { 127 | /** @type { (parts?: string[], limit?: number) => (...more: string[]) => void } */ 128 | const progressFn = (parts = [], limit = 24) => (...more) => { 129 | more.map(abbr(limit)).forEach((s) => { 130 | parts.push(s); 131 | }); 132 | log(...parts); 133 | }; 134 | 135 | const { validator, observer } = shard; 136 | 137 | return freeze({ 138 | /** 139 | * @param {string} src 140 | * @param {string[]} deps 141 | * @returns {Promise<{ src: string, signed: DeployRequest, dataForDeploy: RhoExprWithBlock }>} 142 | */ 143 | async deploy(src, deps) { 144 | const depTargets = deps.map(rhoInfoPath); 145 | const progress = progressFn([src]); 146 | 147 | progress('deps:', `${depTargets.length}`); 148 | const term = await resolveDeps(src, depTargets, { readFile }); 149 | 150 | progress('{', abbr(24)(term), '}', observer.apiBase(), 'after:'); 151 | 152 | const signed = await account.sign(term); 153 | 154 | shard.startProposing(); 155 | 156 | progress( 157 | `${signed.data.validAfterBlockNumber}`, 158 | 'sig:', 159 | signed.signature, 160 | 'deploy', 161 | ); 162 | 163 | const progressAcct = { 164 | sign: account.sign, 165 | async polling() { 166 | await account.polling(); 167 | progress('@'); // (very) short for "still waiting for data at name" 168 | }, 169 | }; 170 | 171 | const deploy = await startTerm(term, validator, observer, progressAcct); 172 | const dataForDeploy = await listenAtDeployId(observer, deploy); 173 | const { expr: result } = dataForDeploy; 174 | 175 | if (!('ExprUri' in result)) { 176 | throw TypeError(`expected URI; got ${result}`); 177 | } 178 | progress(JSON.stringify(result.ExprUri.data)); 179 | shard.stopProposing(); 180 | 181 | return { src, signed, dataForDeploy }; 182 | }, 183 | }); 184 | } 185 | 186 | export function makeContractTask( 187 | TARGETS, 188 | { jake, io: { readFile, writeFile }, shard, account }, 189 | ) { 190 | const mgr = PkgManager(shard, account, { readFile }); 191 | 192 | return function contractTask(src, deps = []) { 193 | const depTargets = deps.map((d) => TARGETS[d]); 194 | jake.desc(`deploy ${src}${deps.length ? ' -> ' : ''}${deps}`); 195 | jake.file(TARGETS[src], [src, ...depTargets], async () => { 196 | const { signed, dataForDeploy } = await mgr.deploy(src, deps); 197 | 198 | await writeFile( 199 | TARGETS[src], 200 | JSON.stringify({ src, signed, dataForDeploy }, null, 2), 201 | ); 202 | }); 203 | }; 204 | } 205 | 206 | /** @type {(txt: string) => {[name: string]: string}} */ 207 | function parseEnv(txt) { 208 | const bindings = txt 209 | .split('\n') 210 | .filter((line) => !line.trim().startsWith('#')) 211 | .map((line) => line.match(/(?\w+)\s*=\s*(?.*)/)) 212 | .filter((parts) => parts && parts.groups) 213 | // @ts-ignore 214 | .map((parts) => [parts.groups.name, parts.groups.value]); 215 | return Object.fromEntries(bindings); 216 | } 217 | 218 | /** 219 | * @param {Record} env 220 | * @param {{ admin?: string, boot: string, read: string }} api 221 | * @param {typeof import('http')} http 222 | * @param {SchedulerAccess} sched 223 | * @param {number=} period 224 | * 225 | * @typedef { { 226 | * setInterval: typeof setInterval, 227 | * clearInterval: typeof clearInterval, 228 | * } } SchedulerAccess 229 | */ 230 | 231 | export function shardAccess(env, api, http, sched, period = 2 * 1000) { 232 | const fetch = nodeFetch({ http }); 233 | 234 | const rnode = RNode(fetch); 235 | 236 | let proposing = false; 237 | let waiters = 0; 238 | let pid; 239 | 240 | return freeze({ 241 | env, 242 | ...api, 243 | validator: rnode.validator(api.boot), 244 | observer: rnode.observer(api.read), 245 | startProposing() { 246 | if (!api.admin) return; 247 | const proposer = rnode.admin(api.admin); 248 | waiters += 1; 249 | if (typeof pid !== 'undefined') { 250 | return; 251 | } 252 | pid = sched.setInterval(() => { 253 | if (!proposing) { 254 | proposing = true; 255 | proposer 256 | .propose() 257 | .then(() => { 258 | console.log('proposed', { waiters }); 259 | proposing = false; 260 | }) 261 | .catch((err) => { 262 | console.log('propose failed', { waiters, err: err.message }); 263 | proposing = false; 264 | }); 265 | } 266 | }, period); 267 | }, 268 | stopProposing() { 269 | if (waiters <= 0) { 270 | return; 271 | } 272 | waiters -= 1; 273 | sched.clearInterval(pid); 274 | pid = undefined; 275 | }, 276 | }); 277 | } 278 | 279 | /** 280 | * Local shard I/O 281 | * 282 | * @param {string} envText 283 | * @param {typeof import('http')} http 284 | * @param {SchedulerAccess} sched 285 | * @param {number} period 286 | */ 287 | export function shardIO(envText, http, sched, period = 2 * 1000) { 288 | const env = parseEnv(envText); 289 | const api = { 290 | admin: `http://${env.MY_NET_IP}:40405`, 291 | boot: `http://${env.MY_NET_IP}:40403`, 292 | read: `http://${env.MY_NET_IP}:40413`, 293 | }; 294 | return shardAccess(env, api, http, sched, period); 295 | } 296 | -------------------------------------------------------------------------------- /src/rnode-openapi-schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * based on: 3 | * https://github.com/rchain/rchain/blob/dev/docs/rnode-api/rnode-openapi-schema.ts 4 | */ 5 | 6 | /** 7 | * RhoExpr 8 | */ 9 | export type RhoExpr = 10 | | ExprMap 11 | | ExprList 12 | | ExprTuple 13 | | ExprPar 14 | | ExprUnforg 15 | | ExprString 16 | | ExprInt 17 | | ExprBool 18 | | ExprUri 19 | | ExprBytes; 20 | 21 | /** 22 | * ExprList 23 | */ 24 | export type ExprList = { ExprList: { data: RhoExpr[] } }; 25 | /** 26 | * ExprPar 27 | */ 28 | export type ExprPar = { ExprPar: { data: RhoExpr[] } }; 29 | /** 30 | * BondInfo 31 | */ 32 | export type BondInfo = { validator: string; stake: number }; 33 | /** 34 | * ExprUri 35 | */ 36 | export type ExprUri = { ExprUri: { data: string } }; 37 | /** 38 | * ExprBytes 39 | */ 40 | export type ExprBytes = { ExprBytes: { data: string } }; 41 | /** 42 | * PrepareRequest 43 | */ 44 | export type PrepareRequest = { 45 | deployer: string; 46 | timestamp: number; 47 | nameQty: number; 48 | }; 49 | /** 50 | * RhoUnforg 51 | */ 52 | export type RhoUnforg = UnforgDeploy | UnforgDeployer | UnforgPrivate; 53 | 54 | /** 55 | * UnforgPrivate 56 | */ 57 | export type UnforgPrivate = { data: string }; 58 | /** 59 | * ExploratoryDeployResponse 60 | */ 61 | export type ExploratoryDeployResponse = { 62 | expr: RhoExpr[]; 63 | block: LightBlockInfo; 64 | }; 65 | /** 66 | * DataResponse 67 | */ 68 | export type DataResponse = { exprs: RhoExprWithBlock[]; length: number }; 69 | /** 70 | * PrepareResponse 71 | */ 72 | export type PrepareResponse = { names: string[]; seqNumber: number }; 73 | /** 74 | * ExprString 75 | */ 76 | export type ExprString = { ExprString: { data: string } }; 77 | /** 78 | * RhoExprWithBlock 79 | */ 80 | export type RhoExprWithBlock = { 81 | expr: RhoExpr; 82 | block: LightBlockInfo; 83 | }; 84 | /** 85 | * LightBlockInfo 86 | */ 87 | export type LightBlockInfo = { 88 | blockHash: string; 89 | justifications: JustificationInfo[]; 90 | timestamp: number; 91 | /** 92 | * com.google.protobuf.ByteString 93 | */ 94 | extraBytes: string; 95 | bonds: BondInfo[]; 96 | parentsHashList: string[]; 97 | shardId: string; 98 | sigAlgorithm: 'secp256k1'; 99 | sig: string; 100 | blockSize: string; 101 | postStateHash: string; 102 | version: number; 103 | seqNum: number; 104 | blockNumber: number; 105 | sender: string; 106 | /** 107 | * com.google.protobuf.ByteString 108 | */ 109 | headerExtraBytes: string; 110 | /** 111 | * com.google.protobuf.ByteString 112 | */ 113 | bodyExtraBytes: string; 114 | faultTolerance: number; 115 | preStateHash: string; 116 | deployCount: number; 117 | }; 118 | /** 119 | * ExploreDeployRequest 120 | */ 121 | export type ExploreDeployRequest = { 122 | term: string; 123 | blockHash: string; 124 | usePreStateHash: boolean; 125 | }; 126 | /** 127 | * DeployData 128 | */ 129 | export type DeployData = { 130 | /** 131 | * conventionally in milliseconds though not constrained by real time 132 | */ 133 | timestamp: number; 134 | term: string; 135 | /** 136 | * bound on computation to be paid for. 137 | */ 138 | phloLimit: number; 139 | /** 140 | * price (in 10^-8 REV) of each unit of computation. typically 1 141 | */ 142 | phloPrice: number; 143 | /** 144 | * number of a recent block 145 | */ 146 | validAfterBlockNumber: number; 147 | }; 148 | /** 149 | * DeployInfo 150 | */ 151 | export type DeployInfo = { 152 | timestamp: number; 153 | /** 154 | * empty string indicates lack of error 155 | */ 156 | systemDeployError: string; 157 | term: string; 158 | phloLimit: number; 159 | sigAlgorithm: 'secp256k1' | 'secp256k1:eth'; 160 | deployer: string; 161 | sig: string; 162 | errored: boolean; 163 | /** 164 | * in units of 10^-8 REV 165 | */ 166 | cost: number; 167 | /** 168 | * in units of 10^-8 REV 169 | */ 170 | phloPrice: number; 171 | validAfterBlockNumber: number; 172 | }; 173 | /** 174 | * JustificationInfo 175 | */ 176 | export type JustificationInfo = { 177 | validator: string; 178 | latestBlockHash: string; 179 | }; 180 | /** 181 | * DeployRequest 182 | */ 183 | export type DeployRequest = { 184 | data: DeployData; 185 | deployer: string; 186 | signature: string; 187 | sigAlgorithm: 'secp256k1' | 'secp256k1:eth'; 188 | }; 189 | /** 190 | * ExprBool 191 | */ 192 | export type ExprBool = { ExprBool: { data: boolean } }; 193 | /** 194 | * UnforgDeploy 195 | */ 196 | export type UnforgDeploy = { UnforgDeploy: { data: string } }; 197 | /** 198 | * BlockInfo 199 | */ 200 | export type BlockInfo = { 201 | blockInfo: LightBlockInfo; 202 | deploys: DeployInfo[]; 203 | }; 204 | /** 205 | * ExprMap 206 | */ 207 | export type ExprMap = { ExprMap: { data: { [key: string]: RhoExpr } } }; 208 | /** 209 | * UnforgDeployer 210 | */ 211 | export type UnforgDeployer = { UnforgDeployer: { data: string } }; 212 | /** 213 | * ExprUnforg 214 | */ 215 | export type ExprUnforg = { ExprUnforg: { data: RhoUnforg } }; 216 | /** 217 | * ExprInt 218 | */ 219 | export type ExprInt = { ExprInt: { data: number } }; 220 | /** 221 | * DataRequest 222 | */ 223 | export type DataRequest = { name: RhoUnforg; depth: number }; 224 | /** 225 | * ExprTuple 226 | */ 227 | export type ExprTuple = { ExprTuple: { data: RhoExpr[] } }; 228 | -------------------------------------------------------------------------------- /src/rnode.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-await-in-loop */ 3 | 4 | /** 5 | @typedef { import('./rnode-openapi-schema').LightBlockInfo } LightBlockInfo 6 | @typedef { import('./rnode-openapi-schema').DataResponse } DataResponse 7 | @typedef { import('./rnode-openapi-schema').BlockInfo } BlockInfo 8 | @typedef { import('./rnode-openapi-schema').DataRequest } DataRequest 9 | @typedef { import('./rnode-openapi-schema').DeployRequest } DeployRequest 10 | @typedef { import('./rnode-openapi-schema').ExploratoryDeployResponse } ExploratoryDeployResponse 11 | 12 | @typedef { { 13 | apiBase(): string, 14 | getBlocks(depth: number): Promise, 15 | listenForDataAtName(request: DataRequest): Promise, 16 | getBlock(hash: string): Promise, 17 | findDeploy(deployId: string): Promise, 18 | exploratoryDeploy(string): Promise, 19 | } } Observer 20 | 21 | @typedef { { 22 | apiBase(): string, 23 | deploy(r: DeployRequest): Promise 24 | } } Validator 25 | 26 | @typedef { { 27 | adminBase(): string, 28 | propose(): Promise 29 | } } RNodeAdmin 30 | */ 31 | 32 | const { freeze } = Object; 33 | 34 | function log(...args) { 35 | if ( 36 | typeof process !== 'undefined' && 37 | typeof process.env !== 'undefined' && 38 | process.env.LOG_RNODE 39 | ) { 40 | console.log(...args); 41 | } 42 | } 43 | 44 | /** 45 | * @param {typeof fetch} fetch 46 | */ 47 | export function RNode(fetch) { 48 | /** 49 | * @param {string} methodUrl 50 | * @param {?Object} request 51 | * @returns {Promise} 52 | */ 53 | async function fetchJSON(methodUrl, request = undefined) { 54 | log({ methodUrl, ...(request === undefined ? {} : { request }) }); 55 | const opts = 56 | request === undefined 57 | ? { method: 'GET' } 58 | : { 59 | method: 'POST', 60 | body: 61 | typeof request === 'string' ? request : JSON.stringify(request), 62 | }; 63 | const resp = await fetch(methodUrl, opts); 64 | let result; 65 | try { 66 | result = await resp.json(); 67 | } catch (err) { 68 | console.error({ methodUrl, request, err }); 69 | throw err; 70 | } 71 | // Add status if server error 72 | if (!resp.ok) { 73 | const ex = new Error(result); 74 | // @ts-ignore 75 | ex.status = resp.status; 76 | throw ex; 77 | } 78 | return result; 79 | } 80 | 81 | return freeze({ 82 | /** @type { (apiBase: string) => Validator } */ 83 | validator: (apiBase) => 84 | freeze({ 85 | apiBase: () => apiBase, 86 | 87 | /** @type { (request: DeployRequest) => Promise } */ 88 | deploy: (request) => fetchJSON(`${apiBase}/api/deploy`, request), 89 | }), 90 | 91 | /** @type { (adminBase: string) => RNodeAdmin } */ 92 | admin: (adminBase) => 93 | freeze({ 94 | adminBase: () => adminBase, 95 | 96 | /** @type { () => Promise } */ 97 | propose: () => fetchJSON(`${adminBase}/api/propose`, ''), 98 | }), 99 | 100 | /** @type { (apiBase: string) => Observer } */ 101 | observer: (apiBase) => 102 | freeze({ 103 | apiBase: () => apiBase, 104 | 105 | /** @type { (request: DataRequest) => Promise } */ 106 | listenForDataAtName: (request) => 107 | fetchJSON(`${apiBase}/api/data-at-name`, request), 108 | 109 | /** @type { (hash: string) => Promise } */ 110 | getBlock: (hash) => fetchJSON(`${apiBase}/api/block/${hash}`), 111 | 112 | /** @type { (depth: number) => Promise } */ 113 | getBlocks: (depth) => fetchJSON(`${apiBase}/api/blocks/${depth}`), 114 | 115 | /** @type { (deployId: string) => Promise } */ 116 | findDeploy: (deployId) => 117 | fetchJSON(`${apiBase}/api/deploy/${deployId}`), 118 | 119 | /** @type { (term: string) => Promise } */ 120 | exploratoryDeploy: (term) => 121 | fetchJSON(`${apiBase}/api/explore-deploy`, term), 122 | }), 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /test/assets.js: -------------------------------------------------------------------------------- 1 | /* assets - treat text files as link-time artifacts. 2 | */ 3 | /* global require, exports */ 4 | // @flow 5 | 6 | const { readFileSync } = require('fs'); 7 | 8 | exports.link = link; 9 | /** 10 | * link a string asset 11 | * 12 | * With respect to ocap discipline, we regard this as part of the 13 | * module loading infrastructure rather than a run-time operation. 14 | * 15 | * An alternative implementation would be a pre-processor 16 | * that in-lines the contents of the linked data as constants. 17 | */ 18 | function link(name /*: string*/) /*: string*/ { 19 | return readFileSync(require.resolve(name), 'utf8'); 20 | } 21 | -------------------------------------------------------------------------------- /test/liveRNodeTest.js: -------------------------------------------------------------------------------- 1 | const { RNode, RHOCore } = require('../index'); 2 | 3 | /** 4 | * log with JSON replacer: stringify Buffer data as hex 5 | */ 6 | module.exports.logged = logged; 7 | function logged(obj /*: mixed */, label /*: ?string */) { 8 | console.log(label, JSON.stringify(obj, bufAsHex, 2)); 9 | return obj; 10 | } 11 | function bufAsHex(prop, val) { 12 | if (prop === 'data' && 'type' in this && this.type === 'Buffer') { 13 | return Buffer.from(val).toString('hex'); 14 | } 15 | return val; 16 | } 17 | 18 | 19 | /** 20 | * Integration test for major features. Requires a running node. 21 | */ 22 | /* 23 | const defaultPayment = { 24 | from: '0x1', 25 | nonce: 0, 26 | phloPrice: 1, 27 | phloLimit: 100000, 28 | }; 29 | 30 | 31 | async function integrationTest({ node, clock }) { 32 | const user = h2b('464f6780d71b724525be14348b59c53dc8795346dfd7576c9f01c397ee7523e6'); 33 | const timestamp = clock().valueOf(); 34 | 35 | // Test deploys and listens 36 | const term = ` 37 | new x, y, z, out(\`rho:io:stdout\`) in { 38 | out!(["names:", *x, *y, *z, *out]) | 39 | x!("x") | y!("y") | z!("z") 40 | } 41 | `; 42 | 43 | const deployData = { term, user, timestamp, ...defaultPayment }; 44 | const preview = await node.previewPrivateIds(deployData, 3); 45 | console.log(preview.map(b2h)); 46 | const deployMessage = await node.doDeploy(deployData); 47 | console.log('doDeploy result:', deployMessage); 48 | 49 | console.log('create: ', await node.createBlock()); 50 | 51 | const idToPar = id => ({ ids: [{ id }] }); 52 | const blockResults = await node.listenForDataAtName(idToPar(preview[0])); 53 | blockResults.forEach((b) => { 54 | b.postBlockData.forEach((d) => { 55 | logged(RHOCore.toJSData(d), 'Data at x'); 56 | }); 57 | }); 58 | } 59 | */ 60 | 61 | ////////////////////////////////////////////////////// 62 | 63 | 64 | /** 65 | * Integration test for major features. Requires a running node. 66 | */ 67 | async function integrationTest({ grpc, endpoint, clock }) { 68 | // Now make an RNode instance 69 | console.log({ endpoint }); 70 | const rchain = RNode(grpc, endpoint); 71 | 72 | // Test deploys and listens 73 | const term = ` 74 | new private, print(\`rho:io:stdout\`) in { 75 | print!(*private) | 76 | private!("Get this text into javascript") | 77 | @"public"!(*private) | 78 | for(@{Int} <- private){Nil} | 79 | for(_ <- @"chan1"; _ <- @"chan2"){Nil} 80 | } 81 | `; 82 | const deployData = { 83 | term, 84 | timestamp: clock().valueOf(), 85 | from: '0x1', 86 | nonce: 0, 87 | phloPrice: 1, 88 | phloLimit: 100000, 89 | }; 90 | 91 | try { 92 | // Deploy term 93 | const deployMessage = await rchain.doDeploy(deployData, true); 94 | console.log('doDeploy result:', deployMessage); 95 | 96 | // Listen for data at public name 97 | let blockResults = await rchain.listenForDataAtPublicName('public'); 98 | const lastBlock = blockResults.slice(-1).pop(); 99 | const privateNameId = await lastBlock.postBlockData.slice(-1).pop(); 100 | 101 | // Listen for data at private name 102 | blockResults = await rchain.listenForDataAtName(privateNameId); 103 | blockResults.forEach((b) => { 104 | b.postBlockData.forEach((d) => { 105 | logged(RHOCore.toRholang(d), 'Data Received from unforgeable name'); 106 | }); 107 | }); 108 | 109 | // Listen for continuation joined public names 110 | blockResults = await rchain.listenForContinuationAtPublicName(['chan1', 'chan2']); 111 | if (blockResults.length > 0) { 112 | console.log('Got continuation at joined public names'); 113 | } else { 114 | console.log('Failed to get continuation at joined public names'); 115 | } 116 | 117 | // Listen for continuation at single private name 118 | // NOTE: This test only passes reliably on a fresh node 119 | // becuase of unintuitive behavior of listen...AtName 120 | // as documented in https://gist.github.com/JoshOrndorff/b0fe7aed93d16beabc2885484c6e8c54 121 | blockResults = await rchain.listenForContinuationAtName([privateNameId], 1); 122 | if (blockResults.length > 0) { 123 | console.log('Got continuation at single private name'); 124 | } else { 125 | console.log('Failed to get continuation at single private name'); 126 | } 127 | } catch (oops) { 128 | console.log(oops); 129 | } 130 | } 131 | 132 | 133 | /////////////////////////////////////////////////////////////////// 134 | 135 | 136 | if (require.main === module) { 137 | // Access ambient stuff only when invoked as main module. 138 | /* eslint-disable global-require */ 139 | const endpoint = { 140 | host: process.env.npm_config_host || 'localhost', 141 | port: parseInt(process.env.npm_config_port || '40401', 10), 142 | }; 143 | 144 | integrationTest( 145 | { 146 | endpoint, 147 | grpc: require('grpc'), 148 | clock: () => new Date(), 149 | }, 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /test/revAddressTest.js: -------------------------------------------------------------------------------- 1 | /* global require*/ 2 | const ttest = require('tape'); // ISSUE: separate tests 3 | const { Hex, REV } = require('..'); 4 | 5 | const { RevAddress } = REV; 6 | 7 | const testCases = [ 8 | { 9 | label: 'all ones', 10 | privateKey: '1111111111111111111111111111111111111111111111111111111111111111', 11 | publicKey: 'd04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737', 12 | revAddress: '11112cFcjtrjwn7qCDvTLMu5jEvMSBN2qT1sBwQxDP9AyQCVi26xKZ', 13 | }, 14 | { 15 | // https://github.com/rchain/rchain/blob/9ae5825aa8b0469372976873b7f229d73060d5fd/rholang/src/test/scala/coop/rchain/rholang/interpreter/util/RevAddressSpec.scala#L11 16 | label: 'RevAddressSpec.scala#L11', 17 | publicKey: '00322ba649cebf90d8bd0eeb0658ea7957bcc59ecee0676c86f4fec517c06251', 18 | revAddress: '1111K9MczqzZrNkUNmNGrNFyz7F7LiCUgaCHXd28g2k5PxiaNuCAi', 19 | }, 20 | ]; 21 | 22 | 23 | testCases.forEach((info) => { 24 | ttest(`RevAddress: ${info.label}`, (t) => { 25 | t.equal(RevAddress.fromPublicKey(Hex.decode(info.publicKey)).toString(), info.revAddress); 26 | t.end(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/target1.rho: -------------------------------------------------------------------------------- 1 | /* See testProxy.js */ 2 | 3 | new 4 | ret, target, 5 | insertArbitrary(`rho:registry:insertArbitrary`), 6 | debug(`rho:io:stderr`) 7 | in { 8 | new uriCh in { 9 | insertArbitrary!(*target, *uriCh) | 10 | for (@uri <- uriCh) { 11 | debug!({"target": uri, "ret": *ret}) | 12 | ret!("${target}" %% {"target": uri}) 13 | } 14 | } 15 | | 16 | contract target(@"buy", @[thing, price], return) = { 17 | debug!({"thing to buy": thing, "price": price}) | 18 | return!({"thing bought": thing, "forPrice": price}) 19 | } 20 | | 21 | contract target(@"sell", @[thing, price, timeout], return) = { 22 | debug!({"thing to sell": thing, "price": price, "timeout": timeout}) | 23 | return!({"sold thing": thing, "for": price, "after": timeout}) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/testSigning.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import test from 'ava'; 4 | 5 | // @flow 6 | import { signPrep } from '../src/deploySig'; 7 | import { Base16 } from '../src/hex.js'; 8 | 9 | export const testVector = [ 10 | { 11 | input: { 12 | keyHex: 13 | 'fd894a416f7157075c5dade8a914099f8d7ab1d0d50533420f67139370f8f562', 14 | deployObj: { 15 | term: 16 | 'new deployId(`rho:rchain:deployId`),\nlog(`rho:io:stdout`)\nin {\n log!("hello") |\n deployId!(1 + 1)\n}\n', 17 | phloLimit: 250000, 18 | phloPrice: 1, 19 | validAfterBlockNumber: 216617, 20 | timestamp: 1592863933369, 21 | }, 22 | }, 23 | expected: { 24 | deploySerialized: 25 | '12666e6577206465706c6f794964286072686f3a72636861696e3a6465706c6f79496460292c0a6c6f67286072686f3a696f3a7374646f757460290a696e207b0a20206c6f6721282268656c6c6f2229207c0a20206465706c6f794964212831202b2031290a7d0a18b987dbf0ad2e38014090a10f50a99c0d', 26 | hashed: 27 | 'e92f8b886c9c39f7c6b8673d83c7b1d9102702c6fc04a0d9a8aac8c1f489604b', 28 | }, 29 | }, 30 | { 31 | input: { 32 | keyHex: 33 | 'fd894a416f7157075c5dade8a914099f8d7ab1d0d50533420f67139370f8f562', 34 | deployObj: { 35 | term: 36 | 'new deployId!(`rho:rchain:deployId`),\nlog(`rho:io:stdout`)\nin {\n log!("hello") |\n deployId!(1 + 1)\n}\n', 37 | phloLimit: 250000, 38 | phloPrice: 1, 39 | validAfterBlockNumber: 216586, 40 | timestamp: 1592862572365, 41 | }, 42 | }, 43 | expected: { 44 | sigArray: 45 | '3045022100ce1bf8355c37fd82f47fa8c87b5a264c09bf5317e0c7d4b2a9d243c991d6928202203380e64d8f30f195c5f1af8429b02540015dd1afce36a5b3e4b951acffb68857', 46 | }, 47 | }, 48 | ]; 49 | 50 | test('signing', (t) => { 51 | for (const { input, expected } of testVector) { 52 | console.log('=== Next test:', input.deployObj.validAfterBlockNumber); 53 | // console.log({ expected }); 54 | const deployInfo = input.deployObj; 55 | const keyHex = input.keyHex; 56 | const actual = signPrep(keyHex, deployInfo); 57 | // console.log({ actual }); 58 | if (expected.sigArray) { 59 | t.is(expected.sigArray, Base16.encode(actual.sigArray)); 60 | } 61 | if (expected.sig) { 62 | t.is(expected.sig, actual.sig); 63 | } 64 | if (expected.deploySerialized) { 65 | t.is(expected.deploySerialized, Base16.encode(actual.deploySerialized)); 66 | } 67 | if (expected.hashed) { 68 | t.is(expected.hashed, actual.hashed); 69 | } 70 | } 71 | }); 72 | --------------------------------------------------------------------------------