Skip to content

Commit 80a5b57

Browse files
authored
feat: service registry v1 (#193)
* service-registry: draft v1 * service-registry: add validations and tests
1 parent 14b517a commit 80a5b57

File tree

6 files changed

+110
-320
lines changed

6 files changed

+110
-320
lines changed

contracts/ServiceRegistry.sol

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,57 @@
11
pragma solidity ^0.6.4;
22
pragma experimental ABIEncoderV2;
33

4-
import "./Governed.sol";
5-
6-
7-
contract ServiceRegistry is Governed {
8-
/*
9-
* @title Graph Protocol Service Registry contract
10-
*
11-
* @author Bryant Eisenbach
12-
* @author Reuven Etzion
13-
*
14-
* @notice Contract Specification:
15-
*
16-
* Requirements ("Service Registry" contract):
17-
* req 01 Maps Ethereum Addresses to URLs
18-
* req 02 No other contracts depend on this, rather is consumed by users of The Graph.
19-
* ...
20-
* @question - Who sets registeredUrls? Staking? (need interface)
21-
*/
224

23-
/* EVENTS */
24-
event ServiceUrlSet(address indexed serviceProvider, string urlString);
5+
/**
6+
* @title ServiceRegistry contract
7+
* @dev This contract supports the service discovery process by allowing indexers to
8+
* register their service url and any other relevant information.
9+
*/
10+
contract ServiceRegistry {
11+
// -- State --
12+
13+
struct IndexerService {
14+
string url;
15+
string geohash;
16+
}
17+
18+
mapping(address => IndexerService) public services;
2519

26-
/* Contract Constructor */
27-
/* @param _governor <address> - Address of the multisig contract as Governor of this contract */
28-
constructor(address _governor) public Governed(_governor) {}
20+
// -- Events --
2921

30-
/* A dynamic array of URLs that bootstrap the graph subgraph
31-
Note: The graph subgraph bootstraps the network. It has no way to retrieve
32-
the list of all indexers at the start of indexing. Therefore a single
33-
dynamic array bootstrapIndexerURLs is used to store the URLS of the Graph
34-
Network indexing nodes for the query node to obtain */
22+
event ServiceRegistered(address indexed indexer, string url, string geohash);
23+
event ServiceUnregistered(address indexed indexer);
3524

36-
// TODO - Who should be able to set this? Right now it is only governance. It needed to more robust, and we need to consider the out of protocol coordination
37-
mapping(address => bytes) public bootstrapIndexerURLs;
25+
/**
26+
* @dev Register an indexer service
27+
* @param _url URL of the indexer service
28+
* @param _geohash Geohash of the indexer service location
29+
*/
30+
function register(string calldata _url, string calldata _geohash) external {
31+
address indexer = msg.sender;
32+
require(bytes(_url).length > 0, "Service must specify a URL");
3833

39-
/* Graph Protocol Functions */
34+
services[indexer] = IndexerService(_url, _geohash);
4035

41-
/*
42-
* @notice Set graph network subgraph indexer URL
43-
* @dev Only governance can do this. Indexers added are arranged out of protocol
44-
*
45-
* @param _indexer <address> - Address of the indexer
46-
* @param _url <string> - URL of the service provider
36+
emit ServiceRegistered(indexer, _url, _geohash);
37+
}
38+
39+
/**
40+
* @dev Unregister an indexer service
4741
*/
48-
function setBootstrapIndexerURL(address _indexer, string calldata _url) external onlyGovernor {
49-
bytes memory url = bytes(_url);
50-
bootstrapIndexerURLs[_indexer] = url;
51-
emit ServiceUrlSet(_indexer, _url);
42+
function unregister() external {
43+
address indexer = msg.sender;
44+
require(isRegistered(indexer), "Service already unregistered");
45+
46+
delete services[indexer];
47+
emit ServiceUnregistered(indexer);
5248
}
5349

54-
/*
55-
* @notice Set service provider url from their address
56-
* @dev Only msg.sender may do this
57-
*
58-
* @param _url <string> - URL of the service provider
50+
/**
51+
* @dev Return the registration status of an indexer service
52+
* @return True if the indexer service is registered
5953
*/
60-
function setUrl(string calldata _url) external {
61-
emit ServiceUrlSet(msg.sender, _url);
54+
function isRegistered(address _indexer) public view returns (bool) {
55+
return bytes(services[_indexer].url).length > 0;
6256
}
6357
}

test/lib/deployment.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const Curation = artifacts.require('./Curation.sol')
33
const DisputeManager = artifacts.require('./DisputeManager')
44
const EpochManager = artifacts.require('./EpochManager')
55
const GraphToken = artifacts.require('./GraphToken.sol')
6+
const ServiceRegisty = artifacts.require('./ServiceRegistry.sol')
67
const Staking = artifacts.require('./Staking.sol')
78

89
// helpers
@@ -39,6 +40,10 @@ function deployEpochManagerContract(owner, params) {
3940
return EpochManager.new(owner, defaults.epochs.lengthInBlocks, params)
4041
}
4142

43+
function deployServiceRegistry(owner) {
44+
return ServiceRegisty.new({ from: owner })
45+
}
46+
4247
async function deployStakingContract(owner, graphToken, epochManager, curation, params) {
4348
const contract = await Staking.new(owner, graphToken, epochManager, curation, params)
4449
await contract.setChannelDisputeEpochs(defaults.staking.channelDisputeEpochs, { from: owner })
@@ -52,5 +57,6 @@ module.exports = {
5257
deployDisputeManagerContract,
5358
deployEpochManagerContract,
5459
deployGraphToken,
60+
deployServiceRegistry,
5561
deployStakingContract,
5662
}

test/serviceRegisty.test.js

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,84 @@
1-
const { expectEvent } = require('@openzeppelin/test-helpers')
1+
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers')
22

33
// contracts
4-
const ServiceRegisty = artifacts.require('./ServiceRegistry.sol')
4+
const deployment = require('./lib/deployment')
55

6-
contract('Service Registry', accounts => {
7-
let deployedServiceRegistry
8-
const governor = accounts[0]
9-
const indexer = accounts[1]
6+
contract('Service Registry', ([governor, indexer]) => {
7+
beforeEach(async function() {
8+
this.serviceRegisty = await deployment.deployServiceRegistry(governor)
9+
this.geohash = '69y7hdrhm6mp'
1010

11-
before(async () => {
12-
// deploy ServiceRegistry contract
13-
deployedServiceRegistry = await ServiceRegisty.new(
14-
governor, // governor
15-
{ from: governor },
16-
)
17-
})
11+
this.shouldRegister = async function(url, geohash) {
12+
// Register the indexer service
13+
const { logs } = await this.serviceRegisty.register(url, geohash, {
14+
from: indexer,
15+
})
1816

19-
it('...should allow setting URL with arbitrary length string', async () => {
20-
const url = 'https://192.168.2.1/'
17+
// Updated state
18+
const indexerService = await this.serviceRegisty.services(indexer)
19+
expect(indexerService.url).to.be.eq(url)
20+
expect(indexerService.geohash).to.be.eq(geohash)
2121

22-
// Set the url
23-
const { logs } = await deployedServiceRegistry.setUrl(url, {
24-
from: indexer,
25-
})
22+
// Event emitted
23+
expectEvent.inLogs(logs, 'ServiceRegistered', {
24+
indexer: indexer,
25+
url: url,
26+
geohash: geohash,
27+
})
28+
}
29+
})
2630

27-
expectEvent.inLogs(logs, 'ServiceUrlSet', {
28-
serviceProvider: indexer,
29-
urlString: url,
31+
describe('register()', function() {
32+
it('should allow registering', async function() {
33+
const url = 'https://192.168.2.1/'
34+
await this.shouldRegister(url, this.geohash)
3035
})
31-
})
3236

33-
it('...should allow setting URL with a very long string', async () => {
34-
const url = 'https://' + 'a'.repeat(125) + '.com'
37+
it('should allow registering with a very long string', async function() {
38+
const url = 'https://' + 'a'.repeat(125) + '.com'
39+
await this.shouldRegister(url, this.geohash)
40+
})
3541

36-
// Set the url
37-
const { logs } = await deployedServiceRegistry.setUrl(url, {
38-
from: indexer,
42+
it('should allow updating a registration', async function() {
43+
const [url1, geohash1] = ['https://thegraph.com', '69y7hdrhm6mp']
44+
const [url2, geohash2] = ['https://192.168.0.1', 'dr5regw2z6y']
45+
await this.shouldRegister(url1, geohash1)
46+
await this.shouldRegister(url2, geohash2)
3947
})
4048

41-
expectEvent.inLogs(logs, 'ServiceUrlSet', {
42-
serviceProvider: indexer,
43-
urlString: url,
49+
it('reject registering empty URL', async function() {
50+
await expectRevert(
51+
this.serviceRegisty.register('', '', {
52+
from: indexer,
53+
}),
54+
'Service must specify a URL',
55+
)
4456
})
4557
})
4658

47-
it('...should allow setting multiple graph bootstrap indexer URLs', async () => {
48-
const url = 'https://192.168.2.1/'
49-
const urlBytes = web3.utils.utf8ToHex(url)
50-
51-
const indexers = accounts.slice(5, 8)
59+
describe('unregister()', function() {
60+
it('should unregister existing registration', async function() {
61+
const url = 'https://thegraph.com'
5262

53-
for (let i = 0; i < 3; i++) {
54-
// Set the url, only governor can
55-
const { logs } = await deployedServiceRegistry.setBootstrapIndexerURL(indexers[i], url, {
56-
from: governor,
63+
// Register the indexer service
64+
await this.serviceRegisty.register(url, this.geohash, {
65+
from: indexer,
5766
})
5867

59-
// Verify that the the ServiceUrlSet event is emitted
60-
expectEvent.inLogs(logs, 'ServiceUrlSet', {
61-
serviceProvider: indexers[i],
62-
urlString: url,
68+
// Unregister the indexer service
69+
const { logs } = await this.serviceRegisty.unregister({ from: indexer })
70+
71+
// Event emitted
72+
expectEvent.inLogs(logs, 'ServiceUnregistered', {
73+
indexer: indexer,
6374
})
75+
})
6476

65-
// Verify that the indexer URL has been updated
66-
const indexerUrlBytes = await deployedServiceRegistry.bootstrapIndexerURLs(indexers[i])
67-
assert(indexerUrlBytes === urlBytes, `Indexer ${i} URL was not set to ${url}`)
68-
}
77+
it('reject unregister non-existing registration', async function() {
78+
await expectRevert(
79+
this.serviceRegisty.unregister({ from: indexer }),
80+
'Service already unregistered',
81+
)
82+
})
6983
})
7084
})

test/subgraph/description.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)