├── .browserslistrc ├── .dockerignore ├── .gitignore ├── Dockerfile ├── Notes.md ├── README.md ├── docker-compose.yml ├── graph.html ├── graph.js ├── index.html ├── package-lock.json ├── package.json └── src ├── admin ├── app │ ├── App.js │ ├── bootstrap.css │ ├── components │ │ ├── ToggleButtonGroup │ │ │ └── index.js │ │ ├── nav │ │ │ └── index.js │ │ ├── pie │ │ │ └── index.js │ │ └── stackedArea │ │ │ └── index.js │ ├── index.css │ ├── index.js │ └── views │ │ ├── SidePanel.js │ │ └── graphBuilder │ │ ├── graph.js │ │ ├── index.js │ │ └── layout-peerId.js ├── engine.js ├── index.js ├── old.js ├── simulation.js ├── utils │ └── copy-to-clipboard.js └── viz │ ├── graph │ ├── base.js │ ├── blocks.js │ ├── ebt.js │ ├── mesh.js │ ├── normal.js │ ├── pie-transport-rx.js │ ├── pie-transport-tx.js │ ├── pie.js │ └── pubsub.js │ └── pie.js ├── client ├── index.js └── libp2p │ ├── createNode.js │ └── peerConnectionTracker.js ├── experiments ├── common │ ├── BaseForceGraph.js │ ├── graph-viz.js │ ├── json.js │ └── obs-store.js ├── debug │ ├── admin.js │ ├── client.js │ ├── uptime.js │ └── versions.js ├── dht │ ├── admin.js │ ├── client.js │ ├── getDhtStats.js │ └── graphs │ │ └── routing.js ├── errors │ ├── admin.js │ ├── client.js │ └── error-log.js ├── peers │ └── client.js ├── platform │ ├── admin.js │ └── client.js └── traffic │ ├── admin.js │ ├── basic.js │ └── client.js ├── partialRollout.js ├── server └── index.js ├── swarm ├── browser.js ├── node.js ├── package-lock.json └── package.json └── util └── colorUtils.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | last 2 Chrome versions 4 | last 2 Firefox versions -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | admin-bundle.js 4 | client-bundle.js 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### app specific 2 | 3 | dist 4 | 5 | 6 | # Created by https://www.gitignore.io/api/osx,node 7 | 8 | ### Node ### 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (http://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Typescript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | 69 | ### OSX ### 70 | *.DS_Store 71 | .AppleDouble 72 | .LSOverride 73 | 74 | # Icon must end with two \r 75 | Icon 76 | 77 | # Thumbnails 78 | ._* 79 | 80 | # Files that might appear in the root of a volume 81 | .DocumentRevisions-V100 82 | .fseventsd 83 | .Spotlight-V100 84 | .TemporaryItems 85 | .Trashes 86 | .VolumeIcon.icns 87 | .com.apple.timemachine.donotpresent 88 | 89 | # Directories potentially created on remote AFP share 90 | .AppleDB 91 | .AppleDesktop 92 | Network Trash Folder 93 | Temporary Items 94 | .apdisk 95 | 96 | .vscode/ 97 | 98 | 99 | # End of https://www.gitignore.io/api/osx,node 100 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | MAINTAINER kumavis 3 | 4 | # setup app dir 5 | RUN mkdir -p /www/ 6 | WORKDIR /www/ 7 | 8 | # install dependencies 9 | COPY ./package.json /www/package.json 10 | RUN npm install 11 | 12 | # copy over app dir 13 | COPY ./ /www/ 14 | 15 | # start server 16 | # CMD npm run server 17 | CMD npm run server:telemetry 18 | 19 | # expose server 20 | EXPOSE 9000 21 | -------------------------------------------------------------------------------- /Notes.md: -------------------------------------------------------------------------------- 1 | - how do i prevent new connections? 2 | - how to tell if a peer supports my protocol? (just attempt dial?) 3 | 4 | - clients are both dialing kitsunet at eachother 5 | - how will we ever get wrtc stable? 6 | - per-protocol connection limits 7 | 8 | - stream each peer's networkState to cnc 9 | - use peerBook for connections list 10 | libp2p.switch.muxedConn 11 | libp2p.getPeers (?) 12 | - on connect, start timeout to check for kitsunet connection 13 | - track pubsub 14 | 15 | - opened https://github.com/libp2p/js-libp2p/issues/175 16 | - we want more low level hooks for rejecting connection attempts 17 | - how will we ever get wrtc stable? 18 | - per-protocol connection limits 19 | 20 | - [ ] network research 21 | - [ ] what to measure to determine network health? 22 | - [ ] what infrastructure to point at (instead of ipfs defaults) 23 | - [ ] how to stand up `ws-star.cloud.ipfs.team` 24 | - [ ] how do we limit peers to avoid crashing 25 | 26 | - [ ] metamask light client 27 | - [ ] block sync 28 | - [ ] how to setup custom protocol/rpc over libp2p 29 | - [ ] pubsub with validation 30 | - [ ] how to shard data 31 | - [ ] how to process new txs to get new state 32 | 33 | - what telemetry is useful to gather 34 | - bandwidth 35 | - location 36 | - memory available? 37 | - connects/disconnects/discoveries 38 | - pubsub spanning tree 39 | - errors: sentry 40 | - @vyzo is working on pubsub 41 | - @JGAntunes is working on pubsub 42 | - https://github.com/ipfs/notes/issues/266 43 | - platform deploy a "job" (eg peering strategy) 44 | - how to stand up `ws-star.cloud.ipfs.team`? 45 | - https://github.com/libp2p/js-libp2p-websocket-star-rendezvous 46 | - how do we limit peers to avoid crashing? 47 | - @pgte is working on this 48 | - application level prioritization of peers 49 | - https://github.com/ipfs/dynamic-data-and-capabilities/issues/3#issuecomment-361919002 50 | - watch repo https://github.com/libp2p/js-libp2p-connection-manager 51 | - how to configure the ipfs node for this experiment? 52 | - just boot libp2p? 53 | 54 | ### eth light client 55 | - GraphSync + selectors 56 | - implement custom protcols 57 | - [dialProtocol](https://github.com/libp2p/js-libp2p#libp2pdialprotocolpeer-protocol-callback) 58 | - [handleProtocol](https://github.com/libp2p/js-libp2p#libp2phandleprotocol-handlerfunc--matchfunc) 59 | - [example protocol - check dialer and listener inside src](https://github.com/libp2p/js-libp2p-identify/) 60 | 61 | 62 | 63 | 64 | bridge (golang) 65 | + peer index 66 | + block syncer 67 | + state bridge 68 | + go-ipfs 69 | ipld selector alpha (handling) <--- daviddias 70 | - state prefetch 71 | - state transition (full) 72 | 73 | metamask (js) <--- daviddias 74 | + block tracking via ipns published head 75 | + ipld selector alpha (requesting) 76 | + broadcast new tx 77 | - log querying ???????? 78 | - peering: bridge 79 | - block syncing 80 | 81 | light client perf hacks 82 | - eth_call proofs (eth_call as query) 83 | - pub/sub storage/logs on full node 84 | - extra-consensus geth bloom filter trie 85 | 86 | 87 | Better eth rpc 88 | - stream data (e.g. logs), with cancel (+ backpressure?) 89 | - selectors 90 | - query trace (anything sent message/eth to me) 91 | 92 | 93 | 94 | 95 | perf: ipld-resolver selector alpha 96 | peering: 1) bridge 2) metamask mesh 97 | new block + tx publishing 98 | coselector indexing 99 | tx -> block 100 | log querying, log -> tx 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### MetaMask light client testing container 2 | 3 | MetaMask currently relies on centralized infrastructure. 4 | We've begun a long journey to turn MetaMask into a light client and bridge this network with the Ethereum mainnet. 5 | 6 | In order to achieve this goal we've created a testing container for deploying p2p experiments in real world environments. These experiments will help us tweak critical p2p components such as peering strategy, state shard distribution, and global pub-sub. 7 | 8 | If you would have any questions or would like to propose an experiment, please open an issue. 9 | 10 | ### Development 11 | 12 | in separate terminal tabs: 13 | ``` 14 | npm start 15 | npm run server 16 | npm run swarm 17 | ``` 18 | 19 | - see `secret` printed out by `npm run server`'s `telemetry` process. 20 | - open `http://localhost:9966/?admin=SECRET` with your secret -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | telemetry: 2 | build: ./ 3 | restart: always 4 | ports: 5 | - "9000" 6 | - "9221:9221" 7 | expose: 8 | - "9221" 9 | environment: 10 | VIRTUAL_PORT: "9000" 11 | VIRTUAL_HOST: "telemetry.lab.metamask.io" 12 | LETSENCRYPT_HOST: "telemetry.lab.metamask.io" 13 | LETSENCRYPT_EMAIL: "admin@metamask.io" 14 | -------------------------------------------------------------------------------- /graph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 23 | 180 | -------------------------------------------------------------------------------- /graph.js: -------------------------------------------------------------------------------- 1 | // const graph = buildGraph(data) 2 | // drawGraph(graph) 3 | const h = require('virtual-dom/h') 4 | const svg = require('virtual-dom/virtual-hyperscript/svg') 5 | const diff = require('virtual-dom/diff') 6 | const patch = require('virtual-dom/patch') 7 | const createElement = require('virtual-dom/create-element') 8 | 9 | const d3 = require('d3') 10 | 11 | module.exports = { 12 | setupDom, 13 | buildGraph, 14 | drawGraph, 15 | } 16 | 17 | function setupDom({ container, action }) { 18 | 19 | let tree = render(h('loading...')) 20 | let rootNode = createElement(tree) 21 | container.appendChild(rootNode) 22 | 23 | function rerender(newTree) { 24 | // const newTree = render(state) 25 | const patches = diff(tree, newTree) 26 | rootNode = patch(rootNode, patches) 27 | tree = newTree 28 | } 29 | 30 | return rerender 31 | 32 | // svg styles 33 | const style = document.createElement('style') 34 | style.textContent = ( 35 | ` 36 | .links line { 37 | stroke: #999; 38 | stroke-opacity: 0.6; 39 | } 40 | 41 | .nodes circle { 42 | stroke: #fff; 43 | stroke-width: 1.5px; 44 | } 45 | 46 | button.refresh { 47 | width: 120px; 48 | height: 30px; 49 | background-color: #4CAF50; 50 | color: white; 51 | border-radius: 3px; 52 | outline: none; 53 | border: 0; 54 | cursor: pointer; 55 | } 56 | 57 | button.refresh:hover { 58 | background-color: green; 59 | } 60 | 61 | .legend { 62 | font-family: "Arial", sans-serif; 63 | font-size: 11px; 64 | } 65 | ` 66 | ) 67 | document.head.appendChild(style) 68 | 69 | // // svg canvas 70 | // const svg = document.createElement('svg') 71 | // container.appendChild(svg) 72 | // svg.setAttribute('width', 960) 73 | // svg.setAttribute('height', 600) 74 | 75 | // action button 76 | const button = document.createElement('button') 77 | button.innerText = 'Refresh Graph' 78 | button.setAttribute("class", "refresh") 79 | button.addEventListener('click', action) 80 | container.appendChild(button) 81 | } 82 | 83 | function buildGraph(data) { 84 | const GOOD = '#1f77b4' 85 | const BAD = '#aec7e8' 86 | const MISSING = '#ff7f0e' 87 | 88 | const graph = { nodes: [], links: [] } 89 | 90 | // first add kitsunet nodes 91 | Object.keys(data).forEach((clientId) => { 92 | const peerData = data[clientId].peers 93 | const badResponse = (typeof peerData !== 'object') 94 | graph.nodes.push({ id: clientId, color: badResponse ? BAD : GOOD }) 95 | }) 96 | 97 | // then links 98 | Object.keys(data).forEach((clientId) => { 99 | const peerData = data[clientId].peers 100 | if (typeof peerData !== 'object') return 101 | Object.keys(peerData).forEach((peerId) => { 102 | // if connected to a missing node, create missing node 103 | const alreadyExists = !!graph.nodes.find(item => item.id === peerId) 104 | if (!alreadyExists) { 105 | graph.nodes.push({ id: peerId, color: MISSING }) 106 | } 107 | // if peer rtt is timeout, dont draw link 108 | const rtt = peerData[peerId] 109 | // if (typeof rtt === 'string') return 110 | const timeout = rtt === 'timeout' 111 | // const linkValue = Math.pow((10 - Math.log(rtt)), 2) 112 | const linkValue = timeout ? 0.1 : 2 113 | graph.links.push({ source: clientId, target: peerId, value: linkValue }) 114 | }) 115 | }) 116 | 117 | return graph 118 | } 119 | 120 | function drawGraph(graph) { 121 | 122 | // 123 | // 124 | // 125 | // 126 | // 127 | // QmbfiPLzd75iiqRzHmnj5tbPTozvWykKcnHAyt7rTL6pMr 128 | // 129 | // 130 | 131 | 132 | var width = 960 133 | var height = 600 134 | var svg = d3.select("body").append("svg") 135 | .attr("width", width) 136 | .attr("height", height) 137 | 138 | // var color = d3.scaleOrdinal(d3.schemeCategory20); 139 | 140 | var simulation = d3.forceSimulation() 141 | .force("link", d3.forceLink().id(function(d) { return d.id; })) 142 | .force("charge", d3.forceManyBody()) 143 | .force("center", d3.forceCenter(width / 2, height / 2)) 144 | .force("x", d3.forceX(width / 2).strength(.05)) 145 | .force("y", d3.forceY(height / 2).strength(.05)) 146 | 147 | var link = svg.append("g") 148 | .attr("class", "links") 149 | .selectAll("line") 150 | .data(graph.links) 151 | .enter().append("line") 152 | .attr("stroke-width", function(d) { return Math.sqrt(d.value); }); 153 | 154 | var node = svg.append("g") 155 | .attr("class", "nodes") 156 | .selectAll("circle") 157 | .data(graph.nodes) 158 | .enter().append("circle") 159 | .attr("r", 5) 160 | .attr("fill", function(d) { return d.color }) 161 | .call(d3.drag() 162 | .on("start", dragstarted) 163 | .on("drag", dragged) 164 | .on("end", dragended)); 165 | 166 | node.append("title") 167 | .text(function(d) { return d.id; }); 168 | 169 | simulation 170 | .nodes(graph.nodes) 171 | .on("tick", ticked); 172 | 173 | simulation.force("link") 174 | .links(graph.links); 175 | 176 | addLegend(); 177 | 178 | function ticked() { 179 | link 180 | .attr("x1", function(d) { return d.source.x; }) 181 | .attr("y1", function(d) { return d.source.y; }) 182 | .attr("x2", function(d) { return d.target.x; }) 183 | .attr("y2", function(d) { return d.target.y; }); 184 | 185 | node 186 | .attr("cx", function(d) { return d.x; }) 187 | .attr("cy", function(d) { return d.y; }); 188 | } 189 | 190 | function dragstarted(d) { 191 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 192 | d.fx = d.x; 193 | d.fy = d.y; 194 | } 195 | 196 | function dragged(d) { 197 | d.fx = d3.event.x; 198 | d.fy = d3.event.y; 199 | } 200 | 201 | function dragended(d) { 202 | if (!d3.event.active) simulation.alphaTarget(0); 203 | d.fx = null; 204 | d.fy = null; 205 | } 206 | 207 | function addLegend() { 208 | var legendData = d3.scaleOrdinal() 209 | .domain(["GOOD - connected to Command N Control (CNC) node", "BAD - bad response", "MISSING - not connected to CNC but known to peers via libp2p"]) 210 | .range([ '#1f77b4', '#aec7e8', '#ff7f0e' ]); 211 | 212 | var svg = d3.select("svg"); 213 | 214 | var legend = svg.append("g") 215 | .attr("class", "legend") 216 | .attr("transform", "translate(20,20)") 217 | 218 | var legendRect = legend 219 | .selectAll('g') 220 | .data(legendData.domain()); 221 | 222 | var legendRectE = legendRect.enter() 223 | .append("g") 224 | .attr("transform", function(d,i){ 225 | return 'translate(0, ' + (i * 20) + ')'; 226 | }); 227 | 228 | legendRectE 229 | .append('path') 230 | .attr("d", d3.symbol().type(d3.symbolCircle)) 231 | .style("fill", function (d,i) { 232 | return legendData(i); 233 | }); 234 | 235 | legendRectE 236 | .append("text") 237 | .attr("x", 10) 238 | .attr("y", 5) 239 | .text(function (d) { 240 | return d; 241 | }); 242 | 243 | } // end addLegend() 244 | 245 | } 246 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | M E T A M A S K M E S H T E S T 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metamask-mesh-testing", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@nodeutils/defaults-deep": "^1.1.0", 8 | "cids": "^0.6.0", 9 | "cors": "^2.8.4", 10 | "d3": "^4.13.0", 11 | "datastore-level": "^0.11.0", 12 | "debug": "^4.1.1", 13 | "deep-equal": "^1.0.1", 14 | "detect-browser": "^4.5.0", 15 | "detect-node": "^2.0.4", 16 | "end-of-stream": "^1.4.1", 17 | "envify": "^4.1.0", 18 | "express": "^4.16.2", 19 | "express-ws": "^4.0.0", 20 | "google-palette": "^1.1.0", 21 | "hat": "0.0.3", 22 | "http-poll-stream": "^2.1.0", 23 | "javascript-time-ago": "^2.0.1", 24 | "kitsunet-telemetry": "^1.1.2", 25 | "libp2p": "^0.25.2", 26 | "libp2p-kad-dht": "github:kumavis/js-libp2p-kad-dht#feat/query-stats", 27 | "kitsunet": "github:musteka-la/kitsunet-js#master", 28 | "libp2p-mplex": "^0.8.5", 29 | "libp2p-webrtc-star": "^0.15.8", 30 | "libp2p-websockets": "^0.12.2", 31 | "localstorage-down": "^0.6.7", 32 | "lodash.toplainobject": "^4.2.0", 33 | "lodash.uniqby": "^4.7.0", 34 | "multihashing": "^0.3.3", 35 | "multihashing-async": "^0.6.0", 36 | "multiplex": "^6.7.0", 37 | "multiplex-rpc": "^1.0.1", 38 | "obs-store": "^3.0.0", 39 | "peer-id": "^0.12.2", 40 | "peer-info": "^0.14.1", 41 | "pify": "^3.0.0", 42 | "promise-to-callback": "^1.0.0", 43 | "promisify-this": "^2.0.2", 44 | "pull-stream": "^3.6.9", 45 | "pull-stream-to-stream": "^1.3.4", 46 | "pump": "^3.0.0", 47 | "push-stream-to-pull-stream": "^1.0.3", 48 | "qs": "^6.5.1", 49 | "raf-throttle": "^2.0.3", 50 | "react": "^16.8.6", 51 | "react-bootstrap": "^1.0.0-beta.8", 52 | "react-dom": "^16.8.6", 53 | "react-force-directed": "^1.1.0", 54 | "react-hyperscript": "^3.2.0", 55 | "rebirth": "^2.0.0", 56 | "recharts": "^1.5.0", 57 | "rpc-stream": "^2.1.2", 58 | "throttle-obj-stream": "^1.0.0", 59 | "through2": "^2.0.3", 60 | "virtual-dom": "^2.1.1", 61 | "webrtcsupport": "^2.2.0", 62 | "websocket-stream": "^5.5.0", 63 | "wrtc": "^0.3.7" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.4.3", 67 | "@babel/preset-env": "^7.4.3", 68 | "@babel/preset-react": "^7.0.0", 69 | "babelify": "^10.0.0", 70 | "browserify": "^16.1.1", 71 | "browserify-css": "^0.14.0", 72 | "budo": "^11.6.2", 73 | "concurrently": "^4.1.0", 74 | "exorcist": "^1.0.1", 75 | "gh-pages": "^2.0.1", 76 | "patch-package": "^5.1.1", 77 | "puppeteer": "^1.14.0", 78 | "wzrd": "^1.5.0" 79 | }, 80 | "browserify": { 81 | "transform": [ 82 | [ 83 | "browserify-css", 84 | { 85 | "autoInject": true 86 | } 87 | ], 88 | [ 89 | "babelify", 90 | { 91 | "presets": [ 92 | "@babel/preset-react", 93 | "@babel/preset-env" 94 | ] 95 | } 96 | ] 97 | ] 98 | }, 99 | "scripts": { 100 | "start": "wzrd src/client/index.js:client-bundle.js src/admin/index.js:admin-bundle.js src/partialRollout.js:partialRollout.js", 101 | "build": "npm run build:clean && npm run build:version && npm run build:base && npm run build:client && npm run build:admin", 102 | "build:clean": "rm -rf dist && mkdir -p dist", 103 | "build:version": "date +%s > dist/BUILD_VERSION; echo \"building $(cat dist/BUILD_VERSION)\"", 104 | "build:base": "cp index.html dist/ && cp src/partialRollout.js dist/", 105 | "build:client": "browserify src/client/index.js --full-paths --debug -t [ envify --BUILD_VERSION \"$(cat dist/BUILD_VERSION)\" ] | exorcist dist/client-bundle.js.map > dist/client-bundle.js", 106 | "build:admin": "browserify src/admin/index.js --full-paths --debug -t [ envify --BUILD_VERSION \"$(cat dist/BUILD_VERSION)\" ] | exorcist dist/admin-bundle.js.map > dist/admin-bundle.js", 107 | "deploy": "npm run build && gh-pages -d dist", 108 | "server": "concurrently -n telemetry,star 'npm run server:telemetry' 'npm run server:star'", 109 | "server:telemetry": "node src/server/index.js", 110 | "server:telemetry:debug": "node --inspect=0.0.0.0:9221 src/server/index.js", 111 | "server:star": "star-signal --port=9090 --host=127.0.0.1", 112 | "server:deploy": "docker-compose pull && docker-compose build && docker-compose stop && docker-compose up -d && docker-compose logs --tail 200 -f", 113 | "swarm": "npm run swarm:node", 114 | "swarm:node": "node src/swarm/node.js", 115 | "swarm:browser": "node src/swarm/browser.js" 116 | }, 117 | "repository": { 118 | "type": "git", 119 | "url": "git+https://github.com/MetaMask/mesh-testing.git" 120 | }, 121 | "author": "", 122 | "license": "ISC", 123 | "bugs": { 124 | "url": "https://github.com/MetaMask/mesh-testing/issues" 125 | }, 126 | "homepage": "https://github.com/MetaMask/mesh-testing#readme" 127 | } 128 | -------------------------------------------------------------------------------- /src/admin/app/App.js: -------------------------------------------------------------------------------- 1 | import './bootstrap.css'; 2 | // import './App.css' 3 | 4 | import React, { Component } from 'react' 5 | import Nav from './components/nav' 6 | const SidePanel = require('./views/SidePanel') 7 | const GraphBuilder = require('./views/graphBuilder') 8 | const dhtExperiment = require('../../experiments/dht/admin') 9 | const errorsExperiment = require('../../experiments/errors/admin') 10 | const debugExperiment = require('../../experiments/debug/admin') 11 | const trafficExperiment = require('../../experiments/traffic/admin') 12 | const platformExperiment = require('../../experiments/platform/admin') 13 | 14 | class App extends Component { 15 | 16 | constructor() { 17 | super() 18 | this.state = { 19 | currentView: 'graphBuilder', 20 | selectedNode: null, 21 | } 22 | this.views = {} 23 | 24 | const graphLayout = { id: 'default:graph', label: 'graph', value: 'graph' } 25 | const circleLayout = { id: 'default:circle', label: 'circle', value: 'circle' } 26 | const xorLayout = { id: 'default:xor', label: 'xor', value: 'xor' } 27 | 28 | this.graphOptions = { 29 | layout: [graphLayout, circleLayout, xorLayout], 30 | topo: [], 31 | color: [], 32 | size: [], 33 | } 34 | 35 | this.views.graphBuilder = { 36 | id: 'graphBuilder', 37 | label: 'custom', 38 | render: ({ store, actions }) => ( 39 | 40 | ) 41 | } 42 | 43 | this.actions = { 44 | selectNode: (clientId) => this.setState({ selectedNode: clientId }), 45 | client: { 46 | sendToClient: async (clientId, method, ...args) => { 47 | const { server } = this.props 48 | // console.log(`sendToClient "${method} - sending...`) 49 | const response = await server.sendToClient(clientId, method, args) 50 | // console.log(`sendToClient "${method}- done`, response) 51 | return response 52 | } 53 | } 54 | } 55 | 56 | this.loadExperiment(trafficExperiment) 57 | this.loadExperiment(dhtExperiment) 58 | this.loadExperiment(errorsExperiment) 59 | this.loadExperiment(debugExperiment) 60 | this.loadExperiment(platformExperiment) 61 | } 62 | 63 | loadExperiment (experiment) { 64 | const { views, graphOptions, actions } = this 65 | const setState = this.setState.bind(this) 66 | experiment({ views, graphOptions, actions, setState }) 67 | // // gather experiment views 68 | // experiment.views.forEach(view => { 69 | // this.views[view.id] = view 70 | // }) 71 | // // gather graph builder components 72 | // const { graphBuilder } = experiment 73 | // if (graphBuilder) { 74 | // this.graphOptions.layout = this.graphOptions.layout.concat(graphBuilder.layout || []) 75 | // this.graphOptions.topo = this.graphOptions.topo.concat(graphBuilder.topo || []) 76 | // this.graphOptions.color = this.graphOptions.color.concat(graphBuilder.color || []) 77 | // this.graphOptions.size = this.graphOptions.size.concat(graphBuilder.size || []) 78 | // } 79 | // // gather experiments 80 | // this.experiments.push(experiment) 81 | } 82 | 83 | selectView (target) { 84 | this.setState(state => ({ currentView: target })) 85 | } 86 | 87 | render () { 88 | const { actions } = this 89 | // const views = Object.values(this.views) 90 | const currentView = this.views[this.state.currentView] 91 | const appState = Object.assign({}, this.state) 92 | 93 | return ( 94 |
95 |
96 | {/*
111 |
112 | 113 |
114 |
115 | ) 116 | } 117 | } 118 | 119 | export default App 120 | -------------------------------------------------------------------------------- /src/admin/app/components/ToggleButtonGroup/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const ToggleButtonGroup = require('react-bootstrap/ToggleButtonGroup') 3 | const ToggleButton = require('react-bootstrap/ToggleButton') 4 | 5 | 6 | class ToggleButtonSelector extends React.Component { 7 | render() { 8 | const { selectedOption, onSelect, options, groupName, labelKey, keyKey } = this.props 9 | 10 | return ( 11 | 17 | {options.map(entry => { 18 | const label = entry[labelKey] 19 | const key = entry[keyKey] 20 | return ( 21 | 25 | {label} 26 | 27 | ) 28 | })} 29 | 30 | ) 31 | } 32 | } 33 | 34 | module.exports = ToggleButtonSelector -------------------------------------------------------------------------------- /src/admin/app/components/nav/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const Tab = require('react-bootstrap/Tab') 3 | const Tabs = require('react-bootstrap/Tabs') 4 | 5 | class NavTabs extends React.Component { 6 | render() { 7 | const { routes } = this.props 8 | return ( 9 | 14 | {routes.map(route => ( 15 | 16 | ))} 17 | 18 | ) 19 | } 20 | } 21 | 22 | export default NavTabs -------------------------------------------------------------------------------- /src/admin/app/components/pie/index.js: -------------------------------------------------------------------------------- 1 | const s = require('react-hyperscript') 2 | const d3 = require('d3') 3 | 4 | module.exports = renderPieChart 5 | 6 | /* 7 | data = [{ 8 | label: String, 9 | value: Number, 10 | }] 11 | */ 12 | 13 | function renderPieChart({ 14 | label, 15 | data, 16 | width, 17 | height, 18 | centerX, 19 | centerY, 20 | innerRadius, 21 | outerRadius, 22 | colors, 23 | renderLabels, 24 | onClick, 25 | }) { 26 | // set defaults 27 | width = width || 220 28 | height = height || 220 29 | centerX = centerX || width/2 30 | centerY = centerY || height/2 31 | outerRadius = outerRadius === undefined ? Math.min(width, height)/2 : outerRadius 32 | innerRadius = innerRadius === undefined ? outerRadius * 0.8 : innerRadius 33 | colors = colors || [ 34 | // green 35 | '#66c2a5', 36 | // blue 37 | '#8da0cb', 38 | // orange 39 | '#fc8d62', 40 | // pink 41 | '#e78ac3', 42 | // lime 43 | '#a6d854', 44 | // yellow 45 | '#ffd92f', 46 | ] 47 | renderLabels = renderLabels === undefined ? true : renderLabels 48 | 49 | // reduce pie chart radii so theres room for labels 50 | const textRadius = outerRadius * 0.9 51 | if (renderLabels) { 52 | innerRadius *= 0.8 53 | outerRadius *= 0.8 54 | } 55 | 56 | // pie chart layout util 57 | const pie = d3.pie() 58 | .value(d => d.value) 59 | .sort(null) 60 | 61 | // edge of pie 62 | const arc = d3.arc() 63 | .innerRadius(innerRadius) 64 | .outerRadius(outerRadius) 65 | 66 | // arc for text labels 67 | const outerArc = d3.arc() 68 | .innerRadius(textRadius) 69 | .outerRadius(textRadius) 70 | 71 | const sliceData = pie(data) 72 | 73 | return ( 74 | 75 | s('g', { 76 | transform: `translate(${centerX}, ${centerY})`, 77 | }, [ 78 | s('g', renderSlices()), 79 | renderLabels ? s('g', renderLabelLines()) : null, 80 | renderLabels ? s('g', renderLabelText()) : null, 81 | label ? renderPrimaryLabel() : null, 82 | ]) 83 | 84 | ) 85 | 86 | function renderSlices() { 87 | return sliceData.map((arcData, index) => { 88 | const fill = colors[index % colors.length] 89 | return s('path', { 90 | fill, 91 | d: arc(arcData), 92 | onClick, 93 | }, [ 94 | s('title', data[index].label), 95 | ]) 96 | }) 97 | } 98 | 99 | function renderLabelLines() { 100 | return sliceData.map((arcData, index) => { 101 | const pos = outerArc.centroid(arcData) 102 | pos[0] = textRadius * (midAngle(arcData) < Math.PI ? 1 : -1) 103 | 104 | return s('polyline', { 105 | points: [arc.centroid(arcData), outerArc.centroid(arcData), pos].join(','), 106 | 'opacity': .3, 107 | 'stroke': 'black', 108 | 'stroke-width': 2, 109 | 'fill': 'none', 110 | }) 111 | }) 112 | } 113 | 114 | function renderLabelText() { 115 | return sliceData.map((arcData, index) => { 116 | const pos = outerArc.centroid(arcData) 117 | // changes the point to be on left or right depending on where label is. 118 | pos[0] = textRadius * (midAngle(arcData) < Math.PI ? 1 : -1) 119 | 120 | return s('text', { 121 | transform: `translate(${pos.join(',')})`, 122 | dy: '0.35em', 123 | style: { 124 | textAnchor: (midAngle(arcData)) < Math.PI ? 'start' : 'end', 125 | }, 126 | }, data[index].label) 127 | }) 128 | } 129 | 130 | function renderPrimaryLabel() { 131 | return s('text', { 132 | transform: `translate(0,0)`, 133 | dy: '0.35em', 134 | style: { 135 | textAnchor: 'middle', 136 | }, 137 | }, label) 138 | } 139 | 140 | function midAngle(d) { 141 | return d.startAngle + (d.endAngle - d.startAngle) / 2 142 | } 143 | } -------------------------------------------------------------------------------- /src/admin/app/components/stackedArea/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, 4 | } from 'recharts'; 5 | 6 | 7 | module.exports = class StackedArea extends PureComponent { 8 | render() { 9 | const { data, direction } = this.props 10 | const dataEntries = Object.entries(data).sort((a,b) => a[0] > b[0] ? 1 : -1) 11 | // transform the data from rows into columns 12 | const timeSeriesLength = 10 13 | // create rows with label 14 | const rows = Array(timeSeriesLength).fill().map((_, index) => { 15 | return { label: `${index*10}s ago` } 16 | }) 17 | // populate rows column by column 18 | dataEntries.forEach(([name, stats]) => { 19 | const timeSeries = stats[direction] 20 | Array(timeSeriesLength).fill().forEach((_, index) => { 21 | rows[index][name] = timeSeries[index] || 0 22 | }) 23 | }) 24 | 25 | const bitrateToLabel = (size) => `${labelForFileSize(size)}/s` 26 | 27 | return ( 28 | 36 | 37 | 38 | 39 | 40 | {this.renderAreas(dataEntries)} 41 | 42 | ) 43 | } 44 | 45 | renderAreas (columns) { 46 | return columns.map(([colName], index) => { 47 | const colors = [ '#8884d8', '#82ca9d', '#ffc658' ] 48 | const color = colors[index % colors.length] 49 | return ( 50 | 58 | ) 59 | }) 60 | } 61 | } 62 | 63 | function labelForFileSize (size) { 64 | const fileSizeOrder = (size === 0) ? 0 : Math.floor((Math.log(size)/Math.log(10))/3) 65 | const fileSizeUnit = ['b','kb','mb'][fileSizeOrder] 66 | const fileSizeForUnit = size / Math.pow(10, fileSizeOrder * 3) 67 | const fileSizeLabel = `${fileSizeForUnit.toFixed(1)} ${fileSizeUnit}` 68 | return fileSizeLabel 69 | } 70 | -------------------------------------------------------------------------------- /src/admin/app/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .App { 7 | display: flex; 8 | flex-direction: row; 9 | flex-wrap: nowrap; 10 | height: 100%; 11 | } 12 | 13 | .AppColumn { 14 | height: 100%; 15 | } 16 | .AppColumn.LeftPanel { 17 | flex-grow: 1; 18 | } 19 | .AppColumn.RightPanel { 20 | flex: 0; 21 | flex-basis: 420px; 22 | max-width: 420px; 23 | overflow-x: hidden; 24 | overflow-y: auto; 25 | border-left: solid 1px #eaeaea; 26 | } -------------------------------------------------------------------------------- /src/admin/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | module.exports = ({ store, serverAsync }) => { 7 | const root = document.createElement('div') 8 | root.id = 'root' 9 | document.body.appendChild(root) 10 | ReactDOM.render(, root); 11 | } -------------------------------------------------------------------------------- /src/admin/app/views/SidePanel.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const h = require('react-hyperscript') 3 | const s = require('react-hyperscript') 4 | const TimeAgo = require('javascript-time-ago') 5 | const TimeAgoEn = require('javascript-time-ago/locale/en') 6 | 7 | const renderPieChart = require('../components/pie') 8 | const StackedArea = require('../components/stackedArea') 9 | 10 | TimeAgo.addLocale(TimeAgoEn) 11 | const timeAgo = new TimeAgo('en-US') 12 | 13 | 14 | class SidePanel extends React.Component { 15 | constructor () { 16 | super() 17 | this.triggerRefresh = () => this.forceUpdate() 18 | } 19 | 20 | componentDidMount () { 21 | const { store } = this.props 22 | store.subscribe(this.triggerRefresh) 23 | } 24 | 25 | componentWillUnmount () { 26 | const { store } = this.props 27 | store.unsubscribe(this.triggerRefresh) 28 | } 29 | 30 | render () { 31 | const { appState, actions, store } = this.props 32 | // cant pass this in from parent or forceUpdate will not use latest state 33 | appState.networkState = store.getState() || {} 34 | return renderSelectedNodePanel(appState, actions) 35 | } 36 | } 37 | 38 | module.exports = SidePanel 39 | 40 | 41 | function renderSelectedNodePanel (state, actions) { 42 | const { selectedNode } = state 43 | 44 | try { 45 | if (selectedNode) { 46 | return renderSelectedNode(state, actions) 47 | } else { 48 | return renderGlobalStats(state, actions) 49 | } 50 | } catch (err) { 51 | return h('pre', err.stack) 52 | } 53 | } 54 | 55 | function renderGlobalStats (state, actions) { 56 | const { networkState } = state 57 | const clientsData = networkState.clients || {} 58 | const clientsCount = Object.keys(clientsData).length 59 | 60 | return ( 61 |
62 | Connected Nodes: {clientsCount} 63 | {/* Nodes: {clientsCount} */} 64 |
65 | ) 66 | } 67 | 68 | function renderSelectedNode (state, actions) { 69 | const { selectedNode, networkState } = state 70 | const clientsData = networkState.clients || {} 71 | const selectedNodeData = clientsData[selectedNode] 72 | // const selectedNodePeers = selectedNodeData.peers 73 | 74 | if (!selectedNode) return null 75 | if (!selectedNodeData) return null 76 | 77 | const shortId = peerIdToShortId(selectedNode) 78 | let versionRelativeTime = getVersionRelativeTime(selectedNodeData.version) 79 | const nodeDebugData = selectedNodeData.debug || {} 80 | const uptime = nodeDebugData.uptime && timeAgo.format(Date.now() - nodeDebugData.uptime) 81 | 82 | const platformData = selectedNodeData.platform 83 | 84 | return ( 85 | 86 | h('div', [ 87 | 88 | // h( 89 | // 'h2', 90 | // `Latest block: ${ 91 | // selectedNodeData.block && typeof selectedNodeData.block.number !== 'undefined' 92 | // ? Number(selectedNodeData.block.number) 93 | // : 'N/A' 94 | // }` 95 | // ), 96 | 97 | h('h2', 'selected node'), 98 | 99 | h('button.app-selected-node', { 100 | onClick: () => copyToClipboard(selectedNode) 101 | }, `id: ${shortId}`), 102 | 103 | h('div', `version: ${selectedNodeData.version} (${versionRelativeTime})`), 104 | h('div', `uptime: ${uptime}`), 105 | 106 | platformData && h('div', [ 107 | `${platformData.name}@${platformData.version} on ${platformData.os}` 108 | ]), 109 | 110 | // h('button', { 111 | // onClick: () => actions.pingNode(selectedNode) 112 | // }, 'ping'), 113 | // h('button', { 114 | // onClick: () => actions.sendPubsub(selectedNode) 115 | // }, 'pubsub'), 116 | // h('button', { 117 | // onClick: () => actions.sendMulticast(selectedNode, 1) 118 | // }, 'multicast 1'), 119 | // h('button', { 120 | // onClick: () => actions.sendMulticast(selectedNode, 3) 121 | // }, 'multicast 3'), 122 | // h('button', { 123 | // onClick: () => actions.sendMulticast(selectedNode, 6) 124 | // }, 'multicast 6'), 125 | // h('button', { 126 | // onClick: () => actions.appendEbtMessage(selectedNode, selectedNodeData.ebtState.sequence) 127 | // }, 'ebt'), 128 | // h('button', { 129 | // onClick: () => actions.restartNode(selectedNode) 130 | // }, 'restart'), 131 | // h('button', { 132 | // onClick: () => { 133 | // selectedNodeData.blockTrackerEnabled = !selectedNodeData.blockTrackerEnabled 134 | // actions.enableBlockTracker(selectedNode, selectedNodeData.blockTrackerEnabled) 135 | // } 136 | // }, `${selectedNodeData.blockTrackerEnabled ? 'disable' : 'enable'} block tracker`), 137 | 138 | h('div', `random walk: ${selectedNodeData.dht.randomWalkEnabled}`), 139 | h('div', `dht group: ${selectedNodeData.dht.group}`), 140 | 141 | h('input', { 142 | id: 'dht-group', 143 | placeholder: 'query dht providers', 144 | onKeyPress: (e) => { 145 | if (e.key !== 'Enter') return 146 | console.log('sending dht query', selectedNode, e.target.value) 147 | actions.dht.performQueryTest(selectedNode, e.target.value, actions) 148 | }, 149 | }), 150 | 151 | h('div', [ 152 | h('button', { 153 | onClick: () => actions.client.sendToClient(selectedNode, 'dht.enableRandomWalk') 154 | }, 'random walk'), 155 | ]), 156 | 157 | h('div', [ 158 | h('button', { 159 | onClick: () => actions.client.sendToClient(selectedNode, 'peers.disconnectAllPeers') 160 | }, 'disconnect all'), 161 | ]), 162 | 163 | h('div', [ 164 | h('button', { 165 | onClick: () => actions.client.sendToClient(selectedNode, 'debug.refresh'), 166 | }, 'restart'), 167 | ]), 168 | 169 | h('div', [ 170 | h('button', { 171 | onClick: () => { 172 | // temp while broadcast is broken 173 | Object.keys(clientsData).map(id => actions.client.sendToClient(id, 'debug.refresh')) 174 | } 175 | }, 'restart all'), 176 | ]), 177 | 178 | // 179 | 180 | // selectedNodePeers && renderSelectedNodePeers(selectedNodePeers), 181 | 182 | renderSelectedNodeStats(selectedNode, state, actions) 183 | 184 | ]) 185 | 186 | ) 187 | } 188 | 189 | function renderSelectedNodeStats (selectedNode, state, actions) { 190 | const networkState = state.networkState || {} 191 | const clientsData = networkState.clients || {} 192 | const selectedNodeData = clientsData[selectedNode] || {} 193 | const trafficStats = selectedNodeData.traffic 194 | if (!trafficStats) return 195 | 196 | return h('div', [ 197 | renderSelectedNodeGlobalStats(trafficStats, state, actions), 198 | h('div', [ 199 | h('h4', 'peers'), 200 | renderSelectedNodePeerStats(trafficStats, state, actions) 201 | ]) 202 | ]) 203 | } 204 | 205 | function renderSelectedNodeGlobalStats (trafficStats, state, actions) { 206 | // global stats 207 | const timeSeries = trafficStats.timeSeries || {} 208 | const globalStats = timeSeries.global 209 | if (globalStats) { 210 | const transports = globalStats.transports 211 | const protocols = globalStats.protocols 212 | return ( 213 | h('div', [ 214 | // renderNodePeerTrafficStatsTimeSeries('transports', transports), 215 | renderNodePeerTrafficStatsTimeSeries('protocols', protocols), 216 | ]) 217 | ) 218 | } else { 219 | return ( 220 | 'no global stats' 221 | ) 222 | } 223 | } 224 | 225 | function renderNodePeerTrafficStatsTimeSeries (label, trafficStats) { 226 | return ['dataSent', 'dataReceived'].map(direction => { 227 | return ([ 228 | h('h3', `${label} - ${direction}`), 229 | h(StackedArea, { 230 | key: direction, 231 | data: trafficStats, 232 | direction, 233 | }) 234 | ]) 235 | }) 236 | } 237 | 238 | function renderSelectedNodePeerStats (trafficStats, state, actions) { 239 | // peer stats 240 | const peers = Object.entries(trafficStats.peers || {}) 241 | return peers.map(([peerId, peerData]) => { 242 | const transports = Object.entries(peerData.transports) 243 | const protocols = Object.entries(peerData.protocols) 244 | // const inGraph = !!state.graph.nodes.find(node => node.id === peerId) 245 | return h('details', { key: peerId }, [ 246 | h('summary', [ 247 | peerIdToShortId(peerId), 248 | h('button', { 249 | // disabled: !inGraph, 250 | onClick: () => actions.selectNode(peerId) 251 | }, 'select') 252 | ]), 253 | // renderNodePeerTrafficStats('transports', transports), 254 | // renderNodePeerTrafficStats('protocols', protocols) 255 | ]) 256 | }) 257 | } 258 | 259 | // function renderNodePeerTrafficStats (label, trafficCategory) { 260 | 261 | // return ( 262 | // h('table', [ 263 | // h('thead', [ 264 | // h('tr', [ 265 | // h('th', label), 266 | // h('th', '1min'), 267 | // h('th', 'all'), 268 | // ]) 269 | // ]), 270 | // h('tbody', [ 271 | // renderRow('in', trafficCategory, 'dataReceived'), 272 | // renderRow('out', trafficCategory, 'dataSent'), 273 | // ]) 274 | // ]) 275 | // ) 276 | 277 | // function renderRow (label, trafficCategory, direction) { 278 | // const size1Min = getTimeLargest(trafficCategory, direction) 279 | // const label1Min = labelForFileSize(size1Min) 280 | // const sizeAll = getSnapshotLargest(trafficCategory, direction) 281 | // const labelAll = labelForFileSize(sizeAll) 282 | // return ( 283 | // h('tr', [ 284 | // h('th', label), 285 | // h('td', [ 286 | // renderNodeStatsPieChart(label1Min, trafficCategory, (stats) => get1Min(stats, direction)), 287 | // ]), 288 | // h('td', [ 289 | // renderNodeStatsPieChart(labelAll, trafficCategory, (stats) => stats.snapshot[direction]) 290 | // ]), 291 | // ]) 292 | // ) 293 | // } 294 | // } 295 | 296 | // function labelForFileSize (size) { 297 | // const fileSizeOrder = Math.floor((Math.log(size)/Math.log(10))/3) 298 | // const fileSizeUnit = ['b','kb','mb'][fileSizeOrder] 299 | // const fileSizeForUnit = size / Math.pow(10, fileSizeOrder * 3) 300 | // const fileSizeLabel = `${fileSizeForUnit.toFixed(1)} ${fileSizeUnit}` 301 | // return fileSizeLabel 302 | // } 303 | 304 | // function getSnapshotLargest (trafficCategory, direction) { 305 | // return trafficCategory.map(([name, stats]) => stats.snapshot[direction]).filter(Boolean).sort().slice(-1)[0] 306 | // } 307 | 308 | // function getTimeLargest (trafficCategory, direction) { 309 | // return trafficCategory.map(([name, stats]) => stats.movingAverages[direction]).filter(Boolean).sort().slice(-1)[0] 310 | // } 311 | 312 | // function get1Min (stats, direction) { 313 | // return stats.movingAverages[direction]['60000'] 314 | // } 315 | 316 | function peerIdToShortId (peerId) { 317 | return peerId && `${peerId.slice(0, 4)}...${peerId.slice(-4)}` 318 | } 319 | 320 | function copyToClipboard (str) { 321 | const el = document.createElement('textarea'); // Create a