# Indexing the RSK blockchain for RIF Marketplace ## Problem Definition Find indexing solutions for the RSK blockchain ## Background Querying data from blockchain runs into performance and bandwidth issues. Primarily, this happens because blockchains do not have an initial query language unlike other regular databases and no record indices are available for optimal queries. Blockchain’s distributed nature also becomes an obstacle in this case. For the lagacy version of the marketplace, a solution was created in order to address this problem for the data existing in the RSK blockchain: [The RIF Marketplace Cache](https://github.com/rsksmart/rif-marketplace-cache). This piece of software appeared to bring some issues: - Little fault tolerance: When it crashes, it gets out of sync with the blockchain data and the server would need to be restarted in order for it to prefetch all of the bockchain's data into the application's database again. However, this problem has been addressed by various third party technologies out there that are dedicated to exclusively indexing blockchains. ## Available solutions According to our research, the problem of indexing the blockchain is being addressed by these technologies: - The Graph - Covalent - Unmarshal - Nakji Network ### The Graph The Graph is one of the most popular ethereum-like blockchain indexing solutions out there that claims to be compatible with Ethereum and IPFS. The Graph serves like a cache for things that have happened in section of the blockchain (subgraph). They store incorporates 3 main concepts in order to address the indexing problem: - **Subgraph**: This can be thought as the specification or manifest that describes What things in the blockchain are goung to be indexed - **Schema**: A GraphQL Schema. This is the specification that indicates the objects or entities we're going to be storing for indexing. - **Mappings**: A set of functions that run when an event is triggered in the blockchain and map the event data to object instances as specified in the schema. By running some tests using Ganache, we're able to see how The Graph tries to fetch data using `eth_blockNumber` and `eth_getBlockByNumber`: ```eth_blockNumber > { > "jsonrpc": "2.0", > "method": "eth_blockNumber", > "params": [], > "id": 586350 > } < { < "id": 586350, < "jsonrpc": "2.0", < "result": "0x43" < } eth_getBlockByNumber > { > "jsonrpc": "2.0", > "method": "eth_getBlockByNumber", > "params": [ > "0x41", > false > ], > "id": 586351 > } < { < "id": 586351, < "jsonrpc": "2.0", < "result": { < "number": "0x41", < "hash": "0xa183434370fa90aab23c011fee987486d0593ee82b7e728c4a2ae9140412db3d", < "parentHash": "0xa7e582a9e30436c7b35f2d5bc652ad34664418e53eafc0d4384e7c7ff18045b8", < "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", < "nonce": "0x0000000000000000", < "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", < "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", < "transactionsRoot": "0x87eb767339b19b099aac8cfa953f624654176fd19294e7df7136c5891606173f", < "stateRoot": "0x8c8bfff286ae3dd093dd47e4df6d26a2511cf0c955961bf73af87654b2566707", < "receiptsRoot": "0xa0d08385ec3b182ec808c662f1ed531b84a515389a00ac2854b0a493522b1b87", < "miner": "0x0000000000000000000000000000000000000000", < "difficulty": "0x0", < "totalDifficulty": "0x0", < "extraData": "0x", < "size": "0x3e8", < "gasLimit": "0x989680", < "gasUsed": "0xaaa2", < "timestamp": "0x6127b526", < "transactions": [ < "0x1f6e1210fede21d395b1b9b9e02290453ed9db29d41a39ea0dbd47e570994ec9" < ], < "uncles": [] < } < } ``` One of RSK's main motivation is to keep as compatible as possible with Ethereum's developer tools, including the JSON-RPC standard endpoints. Diving into [RSK's JSON-RPC Specification](https://developers.rsk.co/rsk/node/architecture/json-rpc/), these and most of the methods seem to be aligned with [Ethereum's JSON-RPC Specification](https://eth.wiki/json-rpc/API). The Hypothesis is that if both blockchains respond to the same interface, then an external service like The Graph shouldn't care about their details but their interface only, in order words, The Graph should be able to work with RSK, since it has the same JSON-RPC interface. In order to test this hypothesis, we will design a simple experiment: Substitute Ganache with a local instance of RSK. It's important to note that there's some background on this, it was tested by another team and it seems that it does work but it's undocumented. For this reason, we will attempt it ourselves in Apps & Services and have everything documented here in case we end up using this solution. What we need for the experiment: 1. Configure a local RSK blockchain node (regtest) 2. Feed some data into the local blockchain (deploy a smart contract) 3. Configure a local graph node connected to the local RSK blockchain 4. Deploy a subgraph that queries the smart contract interaction's data #### Configure a Local RSK blockchain node Provide a basic configuration that allows us to deploy a smart contract and interact with it, as well as exposing this node for a local graph node to index its data. We also want logging functionalities in order to verify what's going on. We will be using RSKj latest release by the time of writing this document: https://github.com/rsksmart/rskj/releases/tag/IRIS-3.0.1 1. Node configuration (regtest.conf) ``` rpc { providers { web { cors = "*" http { enabled = true bind_address = 0.0.0.0 # This appears to be insecure, but didn't work otherwise hosts = ["192.168.65.2", "localhost","host.docker.internal"] # The graph runs inside a docker container, hence this hosts whitelist port = 4444 linger_time = -1 # Not sure what this is } ws { # We're only exposing http (JSON-RPC) enabled = false } } } } miner { server { enabled = true } client { autoMine = true # Not sure about this } } wallet { # Not sure about these enabled = true accounts = [{ privateKey = "6b89a036f50ea2237b8be3800d72c71ac2c8413a8463e47403da50f064d50aab" },{ privateKey = "d8e70f46bcdd6c0437779bad4b927cb9160490620e7c69d9c26dbf7ddbf69701" }] } ``` 2. Logging configuration (logback.xml) ```xml <configuration name="configuration"> <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="execute" level="INFO"/> <logger name="blockvalidator" level="INFO"/> <logger name="blocksyncservice" level="TRACE"/> <logger name="blockexecutor" level="INFO"/> <logger name="general" level="DEBUG"/> <logger name="gaspricetracker" level="ERROR"/> <logger name="web3" level="INFO"/> <logger name="repository" level="ERROR"/> <logger name="VM" level="ERROR"/> <logger name="blockqueue" level="ERROR"/> <logger name="io.netty" level="ERROR"/> <logger name="block" level="ERROR"/> <logger name="minerserver" level="INFO"/> <logger name="txbuilderex" level="ERROR"/> <logger name="pendingstate" level="INFO"/> <logger name="hsqldb.db" level="ERROR"/> <logger name="TCK-Test" level="ERROR"/> <logger name="db" level="ERROR"/> <logger name="net" level="ERROR"/> <logger name="start" level="ERROR"/> <logger name="cli" level="ERROR"/> <logger name="txs" level="ERROR"/> <logger name="gas" level="ERROR"/> <logger name="main" level="ERROR"/> <logger name="trie" level="ERROR"/> <logger name="org.hibernate" level="ERROR"/> <logger name="peermonitor" level="ERROR"/> <logger name="bridge" level="ERROR"/> <logger name="org.springframework" level="ERROR"/> <logger name="rlp" level="ERROR"/> <logger name="messagehandler" level="ERROR"/> <logger name="syncprocessor" level="TRACE"/> <logger name="sync" level="ERROR"/> <logger name="BtcToRskClient" level="ERROR"/> <logger name="ui" level="ERROR"/> <logger name="java.nio" level="ERROR"/> <logger name="org.eclipse.jetty" level="ERROR"/> <logger name="wire" level="ERROR"/> <logger name="BridgeSupport" level="ERROR"/> <logger name="jsonrpc" level="ERROR"/> <logger name="wallet" level="ERROR"/> <logger name="blockchain" level="INFO"/> <logger name="blockprocessor" level="ERROR"/> <logger name="state" level="INFO"/> <logger name="messageProcess" level="INFO"/> <root level="DEBUG"> <appender-ref ref="stdout"/> <appender-ref ref="FILE-AUDIT"/> </root> </configuration> ``` 3. Command for running the node ```shell $ java -Drsk.conf.file=./regtest.conf -Dlogback.configurationFile=./logback.xml -cp rskj-core-3.0.1-IRIS-all.jar co.rsk.Start --regtest ``` #### Feed some data into the local blockchain (deploy a smart contract) For this, we have selected [the ballot smart contract example](https://docs.soliditylang.org/en/v0.4.24/solidity-by-example.html) shown in Ethereum's documentation. 1. Deploy Smart Contract A simple ballot for voting proposals where the contract owner (the chairman) has the capability of giving other wallets the right to vote, and voters can vote for the proposal of their choice. The repository lies [here](https://github.com/jchayan/ethereum-ballot). Modifying the `delegate` and `vote` methods to dispatch events. The idea is to tell The Graph to index these events. ```solidity= pragma solidity ^0.8.0; import "hardhat/console.sol"; contract Ballot { event Voted(address indexed voterId, uint weight, uint proposal); event Delegated(address indexed voterId, address indexed delegateId); function delegate(address to) public { ... emit Delegated(msg.sender, to); } function vote(uint proposal) public { ... emit Voted(msg.sender, voter.weight, voter.vote); } ... } ``` - Events - Delegated(indexed address, indexed address); - Voted(indexed address, uint256, uint256); The contract code can be seen [here](https://github.com/jchayan/ethereum-ballot/blob/main/contracts/Ballot.sol). In order to deploy it, we run: ```shell $ npx hardhat run scripts/deploy.js --network regtest ``` The script's output is configured to give us the contract address and starting block, which we will need later: ``` jorgechayan @ ethereum-ballot$ npx hardhat run scripts/deploy.js --network regtest Compiling 2 files with 0.8.4 Compilation finished successfully Ballot deployed to: 0x77045E71a7A2c50903d88e564cD72fab11e82051 on block number: 1 ``` 2. Contract Interaction A small script has been written in order to simulate voting for 2 proposals: ```= The contract is deployed by the first signer (eth_accounts[0]) The chairman gives signers 1..5 the right to vote Signers 1 and 2 vote for the first proposal Signers 3, 4 and 5 vote for the second proposal We query the the winner (the second proposal, for it has 3 votes) Signers 6, 7 and 8 delegate their vote to Signer 0 (the chairman) The chairman votes for the first proposal with all the weight of the delegated votes. We query the winner (the first proposal, for it has 6 votes) ``` - Emitted events - Every call to `vote` has emitted a `Voted` event - Every call to `delegate` has emitted a `Delegated` event The script code can be seen [here](https://github.com/jchayan/ethereum-ballot/blob/main/scripts/demo.js). In order to run it: ```shell $ node scripts/demo.js --address <contract address> ``` #### Configure a local graph node connected to the local RSK blockchain Following [The Graph's documentation](https://thegraph.com/docs/developer/quick-start): 1. Running a local graph node For this, we need to: * Clone [the graph node repository](https://github.com/graphprotocol/graph-node/) * Configure the graph node docker-compose specification to connect to RSK (the basic settings are provided in file `graph-node/docker/docker-compose.yml`) ```yaml= version: '3' services: graph-node: image: graphprotocol/graph-node ports: - '8000:8000' - '8001:8001' - '8020:8020' - '8030:8030' - '8040:8040' depends_on: - ipfs - postgres environment: postgres_host: postgres postgres_user: graph-node postgres_pass: let-me-in postgres_db: graph-node ipfs: 'ipfs:5001' ethereum: 'mainnet:http://192.168.65.2:4444' # Local IP address + node port - This had to be specified this way due to issues with MacOS and host.docker.internal GRAPH_LOG: info extra_hosts: - "host.docker.internal:host-gateway" # Make host.docker.internal work ipfs: image: ipfs/go-ipfs:v0.4.23 ports: - '5001:5001' volumes: - ./data/ipfs:/data/ipfs postgres: image: postgres ports: - '5432:5432' command: ["postgres", "-cshared_preload_libraries=pg_stat_statements"] environment: POSTGRES_USER: graph-node POSTGRES_PASSWORD: let-me-in POSTGRES_DB: graph-node volumes: - ./data/postgres:/var/lib/postgresql/data ``` * Run the graph node: ```shell $ docker-compose up ``` We should be able to check that the graph node is effectively pulling data from the RSK node: ``` graph-node_1 | Sep 30 00:07:41.392 INFO Downloading latest blocks from Ethereum. This may take a few minutes..., provider: mainnet-rpc-0, component: BlockIngestor graph-node_1 | Sep 30 00:08:06.676 INFO Syncing 14 blocks from Ethereum., code: BlockIngestionStatus, blocks_needed: 14, blocks_behind: 14, latest_block_head: 15, current_block_head: 1, provider: mainnet-rpc-0, component: BlockIngestor ``` On the RSKj node's end, we can see the activity: ``` 10:00:32.209 [nioEventLoopGroup-3-31] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Request: {"jsonrpc":"2.0","method":"eth_blockNumber"} 10:00:32.209 [nioEventLoopGroup-3-31] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Invoking method: eth_blockNumber with args [] 10:00:32.211 [nioEventLoopGroup-3-31] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Invoked method: eth_blockNumber, result 0x0 10:00:33.098 [nioEventLoopGroup-3-32] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Request: {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":279} 10:00:33.098 [nioEventLoopGroup-3-32] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Invoking method: eth_getBlockByNumber with args ["latest", false] 10:00:33.098 [nioEventLoopGroup-3-32] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Invoked method: eth_getBlockByNumber, result org.ethereum.rpc.dto.BlockResultDTO@2b464544 10:00:33.098 [nioEventLoopGroup-3-32] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Response: {"jsonrpc":"2.0","id":279,"result":{"number":"0x0","hash":"0xf75066863a1e84a90cac5f8de8ab3a284a2540953117cae38ab50497a13b4f0c","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0x0d1d2ffa3e93f5fd3a2a82f69be8dd1ea48075bbc20585a8c6cdcf8c28953b65","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","miner":"0x3333333333333333333333333333333333333333","difficulty":"0x1","totalDifficulty":"0x1","extraData":"0x686f727365","size":"0x1de","gasLimit":"0x989680","gasUsed":"0x0","timestamp":"0x0","transactions":[],"uncles":[],"minimumGasPrice":"0x0","bitcoinMergedMiningHeader":"0x00","bitcoinMergedMiningCoinbaseTransaction":"0x00","bitcoinMergedMiningMerkleProof":"0x00","hashForMergedMining":"0xcedfd7a8ccef3b1c9820933971cbd4338ff13993291c437bfcbd4505da401ecf","paidFees":"0x0","cumulativeDifficulty":"0x1"}} 10:00:34.107 [nioEventLoopGroup-3-1] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Request: {"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":280} 10:00:34.107 [nioEventLoopGroup-3-1] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Invoking method: eth_getBlockByNumber with args ["latest", false] 10:00:34.107 [nioEventLoopGroup-3-1] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Invoked method: eth_getBlockByNumber, result org.ethereum.rpc.dto.BlockResultDTO@c7d617f9 10:00:34.107 [nioEventLoopGroup-3-1] DEBUG c.g.jsonrpc4j.JsonRpcBasicServer - Response: {"jsonrpc":"2.0","id":280,"result":{"number":"0x0","hash":"0xf75066863a1e84a90cac5f8de8ab3a284a2540953117cae38ab50497a13b4f0c","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0x0d1d2ffa3e93f5fd3a2a82f69be8dd1ea48075bbc20585a8c6cdcf8c28953b65","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","miner":"0x3333333333333333333333333333333333333333","difficulty":"0x1","totalDifficulty":"0x1","extraData":"0x686f727365","size":"0x1de","gasLimit":"0x989680","gasUsed":"0x0","timestamp":"0x0","transactions":[],"uncles":[],"minimumGasPrice":"0x0","bitcoinMergedMiningHeader":"0x00","bitcoinMergedMiningCoinbaseTransaction":"0x00","bitcoinMergedMiningMerkleProof":"0x00","hashForMergedMining":"0xcedfd7a8ccef3b1c9820933971cbd4338ff13993291c437bfcbd4505da401ecf","paidFees":"0x0","cumulativeDifficulty":"0x1"}} ``` #### Deploy a subgraph that queries the smart contract interaction's data 1. Create the subgraph specification This will be done in the same repository where the Ballot smart contract lies * Subgraph Specification (subgraph.yaml) ```yaml= specVersion: 0.0.2 description: Ethereum Voting System repository: https://github.com/jchayan/ethereum-ballot schema: file: ./schema.graphql dataSources: - kind: ethereum/contract name: Ballot network: mainnet source: address: '0x77045E71a7A2c50903d88e564cD72fab11e82051' # Deployed contract address abi: Ballot startBlock: 1 # We can specify The Graph from which block to start indexing (in this case, from the block the smart contract was deployed) mapping: # Mapping specification kind: ethereum/events # What are we mapping? apiVersion: 0.0.5 language: wasm/assemblyscript entities: - Voter abis: # ABI specification - name: Ballot file: ./artifacts/contracts/Ballot.sol/Ballot.json eventHandlers: # Event-Handler specification (mapping.ts) - event: Voted(indexed address,uint256,uint256) handler: handleVoted - event: Delegated(indexed address,indexed address) handler: handleDelegated file: ./src/mapping.ts # mapping file path ``` * GraphQL schema specification (schema.graphql) We're going to create 2 entities: Vote and Delegation, these entities will be created from the Voted and Delegated events respectivelly (more details on this in the mappings) ```graphql= type Vote @entity { id: ID! weight: BigInt! voterAddress: Bytes! votedProposal: BigInt! } type Delegation @entity { id: ID! voterAddress: Bytes! delegateAddress: Bytes! } ``` * Mapping specification (src/mapping.ts) ```typescript= import { Voted, Delegated } from '../generated/Ballot/Ballot' import { Vote, Delegation } from '../generated/schema' export function handleVoted(event: Voted): void { let params = event.params; let vote = new Vote(params.voterId.toHex()); vote.weight = params.weight; vote.voterAddress = params.voterId; vote.votedProposal = params.proposal; vote.save(); } export function handleDelegated(event: Delegated): void { let params = event.params; let delegation = new Delegation(params.voterId.toHex()); delegation.voterAddress = params.voterId; delegation.delegateAddress = params.delegateId; delegation.save(); } ``` 2. Create the subgraph: ```shell $ graph create ballot --node http://127.0.0.1:8020 ``` In the graph node logs we should see: ``` graph-node_1 | Sep 30 01:40:38.150 INFO Received subgraph_create request, params: SubgraphCreateParams { name: SubgraphName("ballot") }, component: JsonRpcServer ``` 3. Deploy subgraph to the local graph node ``` shell graph deploy ballot --ipfs http://localhost:5001 --node http://127.0.0.1:8020 ``` In the graph node logs we should see: ``` graph-node_1 | Sep 30 01:43:27.934 INFO Received subgraph_deploy request, params: SubgraphDeployParams { name: SubgraphName("ballot"), ipfs_hash: DeploymentHash("Qmbu8tsuLVrAQRkNUAS7reLKK8aPHZ7JCXEqXUFetS5tzv"), node_id: None }, component: JsonRpcServer graph-node_1 | Sep 30 01:43:27.935 INFO Resolve manifest, link: Qmbu8tsuLVrAQRkNUAS7reLKK8aPHZ7JCXEqXUFetS5tzv, sgd: 0, subgraph_id: Qmbu8tsuLVrAQRkNUAS7reLKK8aPHZ7JCXEqXUFetS5tzv, component: SubgraphRegistrar graph-node_1 | Sep 30 01:43:27.945 INFO Resolve schema, link: /ipfs/QmTsb336krYKUBBZSdsU5esvsaWE5R49Rmb8KiBaGVPskA, sgd: 0, subgraph_id: Qmbu8tsuLVrAQRkNUAS7reLKK8aPHZ7JCXEqXUFetS5tzv, component: SubgraphRegistrar ``` #### Results With all of this setup, we're ready to test what our graph node has indexed from the RSK blockchain. For this, we're going to query the subgraph's GraphQL endpoint: 1. Votes ```shell $ curl 'http://localhost:8000/subgraphs/name/ballot' -d '{"query":"{\n votes {\n voterAddress\n votedProposal\n weight\n }\n}","variables":null,"operationName":null}' 2> /dev/null | jsonpp ``` ```json { "data": { "votes": [ { "votedProposal": "0", "voterAddress": "0x0a3aa774752ec2042c46548456c094a76c7f3a79", "weight": "1" }, { "votedProposal": "1", "voterAddress": "0x39b12c05e8503356e3a7df0b7b33efa4c054c409", "weight": "1" }, { "votedProposal": "0", "voterAddress": "0x7986b3df570230288501eea3d890bd66948c9b79", "weight": "1" }, { "votedProposal": "1", "voterAddress": "0xc354d97642faa06781b76ffb6786f72cd7746c97", "weight": "1" }, { "votedProposal": "0", "voterAddress": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "weight": "1" }, { "votedProposal": "1", "voterAddress": "0xcf7cdbbb5f7ba79d3ffe74a0bba13fc0295f6036", "weight": "1" } ] } } ``` 2. Delegations ```shell $ curl 'http://localhost:8000/subgraphs/name/ballot' -d '{"query":"{\n delegations {\n voterAddress\n delegateAddress\n }\n}","variables":null,"operationName":null}' 2> /dev/null | jsonpp ``` ```json { "data": { "delegations": [ { "delegateAddress": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "voterAddress": "0x7857288e171c6159c5576d1bd9ac40c0c48a771c" }, { "delegateAddress": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "voterAddress": "0xa4dea4d5c954f5fd9e87f0e9752911e83a3d18b3" }, { "delegateAddress": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "voterAddress": "0xdebe71e1de41fc77c44df4b6db940026e31b0e71" } ] } } ``` Conclusions to be added here...