Shasper viz 0.4 visualization of ethereum sharding. work in progress. forked from https://beta.observablehq.com/@artyomtrityak/3d-force-graph-nodes-clicks-and-selection
gDataBeacon = { const N = 1; return { nodes: [...Array(N).keys()].map(i => ({ id: i, fx: 0, fy: i*20, fz: 0, arrow: "yes", chain: "beacon" })), links: [...Array(N).keys()] .filter(id => id > 0) .map(id => ({ source: id, target: id-1 })) }; }
gData = gDataBeacon
NUM_SHARDS = 8
CYCLE_LENGTH = 8 // aka number of blocks per epoch
SHARD_NUM_TO_LETTER = ({ 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', 6: 'F', 7: 'G', 8: 'H' })
SHARD_LETTER_TO_NUM = ({ 'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7, 'H': 8 })
BLOCK_Y_SPACING = 20
function getShardPosition(shard_i, block_i) { const RADIUS = 50; const full_radians = Math.PI * 2; const div_radians = full_radians / NUM_SHARDS; const shard_n_radians = div_radians * shard_i; const x_pos = Math.cos(shard_n_radians) * RADIUS; const z_pos = Math.sin(shard_n_radians) * RADIUS; return {x: Math.round(x_pos), z: Math.round(z_pos)}; }
function addBeaconBlock(block_i) { let finalized = 'no'; const new_beacon_block = { //id: shardName + '-' + block_i, // shardA-n id: block_i, // beacon-n fx: 0, fy: block_i * BLOCK_Y_SPACING, fz: 0, arrow: "yes", chain: "beacon", finalized: finalized }; gData.nodes.push(new_beacon_block); if (block_i > 0) { const new_beacon_link = { source: block_i, target: (block_i - 1) }; gData.links.push(new_beacon_link); } }
function addShardBlockToGraph(shard_i, block_i) { const shardName = 'shard' + SHARD_NUM_TO_LETTER[shard_i]; const shard_block_pos = getShardPosition(shard_i, block_i); const new_shard_block = { id: shardName + '-' + block_i, // shardA-n //fx: 50, fy: i*20, fz: 0, arrow: "yes", chain: "shard" fx: shard_block_pos.x, fy: block_i * BLOCK_Y_SPACING, fz: shard_block_pos.z, arrow: "yes", chain: "shard" }; gData.nodes.push(new_shard_block); if (block_i > 0) { const new_shard_link = { source: shardName + '-' + block_i, target: shardName + '-' + (block_i - 1) }; gData.links.push(new_shard_link); } }
function addBunchOfShardBlocks() { addShardBlockToGraph(1,0) addShardBlockToGraph(2,0) addShardBlockToGraph(3,0) addShardBlockToGraph(4,0) addShardBlockToGraph(5,0) addShardBlockToGraph(6,0) addShardBlockToGraph(7,0) addShardBlockToGraph(8,0) /* addShardBlockToGraph(1,1) addShardBlockToGraph(2,1) addShardBlockToGraph(3,1) addShardBlockToGraph(4,1) addShardBlockToGraph(5,1) addShardBlockToGraph(6,1) addShardBlockToGraph(7,1) addShardBlockToGraph(8,1) */ /* addShardBlockToGraph(1,2) addShardBlockToGraph(2,2) addShardBlockToGraph(3,2) addShardBlockToGraph(4,2) addShardBlockToGraph(5,2) */ }
// add initial shard genesis blocks addBunchOfShardBlocks()
function addCrosslink(beacon_block_i, shard_i, shard_block_i) { const shardName = 'shard' + SHARD_NUM_TO_LETTER[shard_i]; const new_cross_link = { source: beacon_block_i, target: shardName + '-' + (shard_block_i), isCrossLink: "yes" }; gData.links.push(new_cross_link); }
function finalizeAncestors(beacon_block_i) { gData.nodes.forEach(d => { let finalized_beacon_slot = beacon_block_i - CYCLE_LENGTH; // finalized block is one cycle back let finalized_shard_num = (finalized_beacon_slot % NUM_SHARDS) + 1; let finalized_shard_block = finalized_beacon_slot - 3; // crosslink 3 (arbitrary) slots ago if (d.chain === "beacon") { // id: n if (d.id <= finalized_beacon_slot) { d.finalized = "yes"; } } if (d.chain === "shard") { //console.log('checking shard block:', d) // id: shardA-n let id = d.id.toString(); let shard_letter = id.slice(-3, -2); let shard_num = SHARD_LETTER_TO_NUM[shard_letter]; let shard_block_i = parseInt(id.slice(-1)); if ((shard_num === finalized_shard_num) && (shard_block_i <= finalized_shard_block)) { //console.log('shard block d.id: ' + d.id + ' setting finalized.'); d.finalized = "yes"; } } return d; }); }
Notes The actual beacon chain is forkful (see a visualization of beacon chain forks). The view here is idealized and doesn't show the beacon chain micro-forks; only the single finalized beacon chain is shown. Each beacon block contains a random number. That random number is used to elect one validator per shard: the shard block proposers at that height, or "slot". Thus, the beacon block appears first, then the shard blocks appear (more or less simultaneously, in a random order), then the next beacon block, and so on. Crosslinks are attestations by a committee of validators (ShardAndCommittee) to data availability of all shard blocks on a chain, since that chain's last crosslink. The validator set that makes up the crosslink attestation committee for a shard is a different set from the validators simply referred to as "shard validators" (i.e. the shard's block proposers). legend Beacon chain - DodgerBlue, blocks in the center Shard chains - AquaMarine, positioned in a circle with the beacon chain in the center Crosslinks - blue arrows between beacon blocks and shard blocks Finalized blocks - Gold. both beacon blocks and shard blocks become finalized after one cycle (still buggy).
htmlContainer = html`<div id="3d-graph" style="width: 700px; height: 600px;">Your browser does not support WebGL</div>`
Graph = { const Graph = ForceGraph3D()(htmlContainer) .enableNodeDrag(false) .cameraPosition({x: 0, y: 200, z: 200}, {x: 0, y: 50, z: 0}) //simulation.force("charge", null); .d3Force("charge", null) //.d3Force("link", null) .width(700) .height(600) .backgroundColor("#DCDCDC") .linkColor((d) => { return "#2F4F4F"; }) .nodeThreeObject((d) => { //console.log('node Three object d:', d) var color = 0xbf2e2e; if (d.finalized === "yes") { color = 0xFFD700; // Gold } else if (d.chain === "beacon") { // unfinalized beacon block color = 0x1E90FF; // DodgerBlue } else { // unfinalized shard block color = 0x7FFFD4; // AquaMarine } //const geometry = new THREE.DodecahedronGeometry(5); const geometry = new THREE.BoxGeometry(5,5,5); const material = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: 0.9 }) const object = new THREE.Mesh(geometry, material); return object; }) /* .nodeColor(d => { console.log('nodeColor d:', d) if (d.chain === "beacon") { return "blue"; } else { return "#FFD700"; // Gold } }) */ .graphData(gData) .linkMaterial(d => { /* const material = new THREE.LineDashedMaterial( { color: 0xff0000, linewidth: 1, scale: 2, dashSize: 3, gapSize: 2, }); return material; const material = new THREE.LineDashedMaterial({ color: '#000', dashSize: 2, gapSize: 2, fog:false, depthWrite : false }) return material; // LineDashedMaterial is not working due to some bug // see the fiddle where grid.computeLineDistances() is called to get dashes working // https://jsfiddle.net/chzmotju/ // computeLineDistances trick is taken from https://github.com/mrdoob/three.js/issues/8494 */ //var defaultLinkColor = 0x000000; const CrosslinkColor = 0x00CED1; // DarkTurquoise var linkColor = 0x000000; // default if (d.isCrossLink === "yes") { linkColor = CrosslinkColor; } const material = new THREE.LineBasicMaterial( { color: linkColor, linewidth: 1, linecap: 'round', //ignored by WebGLRenderer linejoin: 'round' //ignored by WebGLRenderer } ); return material; }) .linkDirectionalArrowLength(d => { //console.log('linkDirectionalArrowLength d:', d); if (d.source.arrow == "yes") { return 3.5; } else { return 0; } }) .linkDirectionalArrowRelPos(1); /* Graph.onNodeClick((d) => { d.__threeObj.traverse(child => { if (child instanceof THREE.Mesh) { const color = child.material.__selected ? 0xbf2e2e : 0xe8ff78; child.material.__selected = !child.material.__selected; child.material.setValues({ color }); } }); }) */ return Graph; }
function showLegend() { let vizLegend = document.createElement('div') vizLegend.style.marginBottom = "20px"; vizLegend.style.left = "-120px"; htmlContainer.appendChild(vizLegend) vizLegend.className = 'graph-nav-info'; vizLegend.textContent = "Beacon Blocks are DodgerBlue. Shard Blocks are AquaMarine."; }
{ // Weird issue only reproduced at observablehq :\ await Promises.delay(100); Graph.graphData(gData); }
{ // show the legend await Promises.delay(500); showLegend() }
function addAllShardBlocks(block_i) { for (let shard_i = 1; shard_i <= NUM_SHARDS; shard_i++) { //for (let shard_i = 1; shard_i <= NUM_SHARDS; shard_i++) { setTimeout(function() { console.log('adding shard_i, block_i', shard_i, block_i); addShardBlockToGraph(shard_i, block_i); Graph.graphData(gData); }, randomIntFromInterval(800, 2700)); } //console.log('adding shard_i, block_i', shard_i, block_i); }
BeaconTimer = { let beacon_subtick_i = 0; let timer_i = 0; // start at -1 if you want to redraw block 0 for all shards let beacon_timer = 0; function beaconTick() { // subticks are because yield must be called every 1s. // but we only want to tick every 5s. (faster - 3s) beacon_subtick_i = beacon_subtick_i + 1; if (beacon_subtick_i < 5) { return timer_i; } beacon_subtick_i = 0; timer_i = timer_i + 1; beacon_timer = beacon_timer + 1; addBeaconBlock(beacon_timer); if (beacon_timer > 3) { let crosslink_shard_i = (beacon_timer % NUM_SHARDS) + 1; let crosslink_block_i = beacon_timer - 3; // crosslink 3 (arbitrary) slots ago //addCrosslink(5, 1, 1) // add crosslink from beacon block 5 to shardA-block1 addCrosslink(beacon_timer, crosslink_shard_i, crosslink_block_i); // finalize ancestors after adding the crosslink finalizeAncestors(beacon_timer); } Graph.graphData(gData); addAllShardBlocks(beacon_timer); return timer_i; } while (timer_i < 75) { const then = Math.ceil((Date.now() + 1) / 1000) * 1000; // tick once per second yield Promises.when(then, then).then(beaconTick); } }
function randomIntFromInterval(min,max){ return Math.floor(Math.random()*(max-min+1)+min); }
THREE = require("three@0.86.0")
ForceGraph3D = require("3d-force-graph@1.39.0")