Skip to content

Commit f8a3669

Browse files
authored
disputes: attestation update to latest format (#188)
* disputes: - update the attestation format to match the one sent by index node - add an attestation parser - set graph protocol to zero in domain separator hash - emit the signed attestation instead of the bare one * disputes: - remove BytesLib dependency and use Solidity 0.6 array slice - remove ECDSA-openzeppelin dependency for ecrecover() - refactor Attestation struct to follow RFC - change language around attestation and receipts - ignore linting of the DisputeManager till support for array slices is added * disputes: use receipt instead of attestation according to the RFC * disputes: reorder attestation struct attributes to match RFC and signature sent by clients * disputes: refactor signature parsing and update client library to send signatures * disputes: create dispute ID by hashing receipt + indexer address
1 parent 52df003 commit f8a3669

File tree

8 files changed

+350
-261
lines changed

8 files changed

+350
-261
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
contracts/DisputeManager.sol

.soliumignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules
22
contracts/Staking.sol
33
contracts/Migrations.sol
4+
contracts/DisputeManager.sol
45
contracts/bancor
56
contracts/openzeppelin
67
contracts/MultiSigWallet.sol

contracts/Curation.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ contract Curation is Governed, BancorFormula {
4848
// Mapping of subgraphID => Subgraph
4949
mapping(bytes32 => Subgraph) public subgraphs;
5050

51-
// Address of a staking contract that will distribute fees to subgraph reserves
51+
// Address of the staking contract that will distribute fees to subgraph reserves
5252
address public staking;
5353

5454
// Token used for staking

contracts/DisputeManager.sol

Lines changed: 166 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@ pragma experimental ABIEncoderV2;
44
import "./Governed.sol";
55
import "./GraphToken.sol";
66
import "./Staking.sol";
7-
import "./bytes/BytesLib.sol";
8-
import "@openzeppelin/contracts/cryptography/ECDSA.sol";
97

108

119
/*
1210
* @title DisputeManager
1311
* @dev Provides a way to align the incentives of participants ensuring that query results are trustful.
1412
*/
1513
contract DisputeManager is Governed {
16-
using BytesLib for bytes;
17-
using ECDSA for bytes32;
1814
using SafeMath for uint256;
1915

2016
// Disputes contain info neccessary for the Arbitrator to verify and resolve
@@ -27,44 +23,29 @@ contract DisputeManager is Governed {
2723

2824
// -- Attestation --
2925

30-
// Store IPFS hash as 32 byte hash and 2 byte hash function
31-
// Note: Not future proof against IPFS planned updates to support multihash, which would require a len field
32-
// Note: hashFunction - 0x1220 is 'Qm', or SHA256 with 32 byte length
33-
struct IpfsHash {
34-
bytes32 hash;
35-
uint16 hashFunction;
36-
}
37-
38-
// Signed message sent from IndexNode in response to a request
39-
// Note: Message is located at the given IPFS content address
26+
// Attestation sent from IndexNode in response to a request
4027
struct Attestation {
41-
// Content Identifier for request message sent from user to indexing node
42-
IpfsHash requestCID;
43-
// Content Identifier for signed response message from indexing node
44-
IpfsHash responseCID;
45-
// Amount of computational account units (gas) used to process query
46-
uint256 gasUsed;
47-
// Amount of data sent in the response
48-
uint256 responseNumBytes;
49-
// ECDSA vrs signature (using secp256k1)
28+
bytes32 requestCID;
29+
bytes32 responseCID;
30+
bytes32 subgraphID;
5031
uint8 v;
5132
bytes32 r;
5233
bytes32 s;
5334
}
5435

55-
uint256 private constant ATTESTATION_SIZE_BYTES = 192;
56-
uint256 private constant SIGNATURE_SIZE_BYTES = 65;
36+
uint256 private constant ATTESTATION_SIZE_BYTES = 161;
37+
uint256 private constant RECEIPT_SIZE_BYTES = 96;
5738

5839
// -- EIP-712 --
5940

6041
bytes32 private constant DOMAIN_TYPE_HASH = keccak256(
61-
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
42+
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"
6243
);
6344
bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Protocol");
64-
bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0.1");
45+
bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0");
6546
bytes32 private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2;
66-
bytes32 private constant ATTESTATION_TYPE_HASH = keccak256(
67-
"Attestation(IpfsHash requestCID,IpfsHash responseCID,uint256 gasUsed,uint256 responseNumBytes)IpfsHash(bytes32 hash,uint16 hashFunction)"
47+
bytes32 private constant RECEIPT_TYPE_HASH = keccak256(
48+
"Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphID)"
6849
);
6950

7051
// 100% in parts per million
@@ -103,6 +84,10 @@ contract DisputeManager is Governed {
10384

10485
// -- Events --
10586

87+
/**
88+
* @dev Emitted when `disputeID` is created for `subgraphID` and `indexNode` by `fisherman`.
89+
* The event emits the amount `tokens` deposited by the fisherman and `attestation` submitted.
90+
*/
10691
event DisputeCreated(
10792
bytes32 disputeID,
10893
bytes32 indexed subgraphID,
@@ -112,6 +97,11 @@ contract DisputeManager is Governed {
11297
bytes attestation
11398
);
11499

100+
/**
101+
* @dev Emitted when arbitrator accepts a `disputeID` for `subgraphID` and `indexNode`
102+
* created by `fisherman`.
103+
* The event emits the amount `tokens` transferred to the fisherman, the deposit plus reward.
104+
*/
115105
event DisputeAccepted(
116106
bytes32 disputeID,
117107
bytes32 indexed subgraphID,
@@ -120,6 +110,11 @@ contract DisputeManager is Governed {
120110
uint256 tokens
121111
);
122112

113+
/**
114+
* @dev Emitted when arbitrator rejects a `disputeID` for `subgraphID` and `indexNode`
115+
* created by `fisherman`.
116+
* The event emits the amount `tokens` burned from the fisherman deposit.
117+
*/
123118
event DisputeRejected(
124119
bytes32 disputeID,
125120
bytes32 indexed subgraphID,
@@ -128,6 +123,11 @@ contract DisputeManager is Governed {
128123
uint256 tokens
129124
);
130125

126+
/**
127+
* @dev Emitted when arbitrator draw a `disputeID` for `subgraphID` and `indexNode`
128+
* created by `fisherman`.
129+
* The event emits the amount `tokens` used as deposit and returned to the fisherman.
130+
*/
131131
event DisputeIgnored(
132132
bytes32 disputeID,
133133
bytes32 indexed subgraphID,
@@ -211,20 +211,21 @@ contract DisputeManager is Governed {
211211
}
212212

213213
/**
214-
* @dev Get the hash of encoded message to use as disputeID
215-
* @notice Return the disputeID for a particular attestation
216-
* @param _attestation Signed Attestation message
217-
* @return Hash of encoded message used as disputeID
214+
* @dev Get the message hash that an indexer used to sign the receipt.
215+
* Encodes a receipt using a domain separator, as described on
216+
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification.
217+
* @notice Return the message hash used to sign the receipt
218+
* @param _receipt Receipt returned by indexer and submitted by fisherman
219+
* @return Message hash used to sign the receipt
218220
*/
219-
function getDisputeID(bytes memory _attestation) public view returns (bytes32) {
220-
// TODO: add a nonce?
221+
function encodeHashReceipt(bytes memory _receipt) public view returns (bytes32) {
221222
return
222223
keccak256(
223224
abi.encodePacked(
224225
"\x19\x01", // EIP-191 encoding pad, EIP-712 version 1
225226
DOMAIN_SEPARATOR,
226227
keccak256(
227-
abi.encode(ATTESTATION_TYPE_HASH, _attestation) // EIP 712-encoded message hash
228+
abi.encode(RECEIPT_TYPE_HASH, _receipt) // EIP 712-encoded message hash
228229
)
229230
)
230231
);
@@ -281,7 +282,7 @@ contract DisputeManager is Governed {
281282
/**
282283
* @dev Accept tokens
283284
* @notice Receive Graph tokens
284-
* @param _from Token holder's address
285+
* @param _from Token sender address
285286
* @param _value Amount of Graph Tokens
286287
* @param _data Extra data payload
287288
*/
@@ -292,18 +293,8 @@ contract DisputeManager is Governed {
292293
// Make sure the token is the caller of this function
293294
require(msg.sender == address(token), "Caller is not the GRT token contract");
294295

295-
// Decode subgraphID
296-
bytes32 subgraphID = _data.slice(0, 32).toBytes32(0);
297-
298-
// Decode attestation
299-
bytes memory attestation = _data.slice(32, ATTESTATION_SIZE_BYTES);
300-
require(attestation.length == ATTESTATION_SIZE_BYTES, "Signature must be 192 bytes long");
301-
302-
// Decode attestation signature
303-
bytes memory sig = _data.slice(32 + ATTESTATION_SIZE_BYTES, SIGNATURE_SIZE_BYTES);
304-
require(sig.length == SIGNATURE_SIZE_BYTES, "Signature must be 65 bytes long");
305-
306-
_createDispute(attestation, sig, subgraphID, _from, _value);
296+
// Create a dispute using the received attestation
297+
_createDispute(_data, _from, _value);
307298

308299
return true;
309300
}
@@ -397,25 +388,29 @@ contract DisputeManager is Governed {
397388

398389
/**
399390
* @dev Create a dispute for the arbitrator to resolve
400-
* @param _attestation Attestation message
401-
* @param _sig Attestation signature
402-
* @param _subgraphID subgraphID for the attestation message
391+
* @param _attestationData Attestation bytes submitted by the fisherman
403392
* @param _fisherman Creator of dispute
404393
* @param _deposit Amount of tokens staked as deposit
405394
*/
406-
function _createDispute(
407-
bytes memory _attestation,
408-
bytes memory _sig,
409-
bytes32 _subgraphID,
410-
address _fisherman,
411-
uint256 _deposit
412-
) private {
413-
// Obtain the hash of the fully-encoded message, per EIP-712 encoding
414-
bytes32 disputeID = getDisputeID(_attestation);
395+
function _createDispute(bytes memory _attestationData, address _fisherman, uint256 _deposit) private {
396+
// Check attestation data length
397+
require(_attestationData.length == ATTESTATION_SIZE_BYTES, "Attestation must be 161 bytes long");
415398

416-
// Obtain the signer of the fully-encoded EIP-712 message hash
417-
// Note: The signer of the attestation is the indexNode that served it
418-
address indexNode = disputeID.recover(_sig);
399+
// Decode attestation
400+
Attestation memory attestation = _parseAttestation(_attestationData);
401+
402+
// Get attestation signer
403+
address indexNode = _recoverAttestationSigner(attestation);
404+
405+
// Create a disputeID
406+
bytes32 disputeID = keccak256(
407+
abi.encodePacked(
408+
attestation.requestCID,
409+
attestation.responseCID,
410+
attestation.subgraphID,
411+
indexNode
412+
)
413+
);
419414

420415
// This also validates that index node exists
421416
require(staking.hasStake(indexNode), "Dispute has no stake by the index node");
@@ -427,9 +422,35 @@ contract DisputeManager is Governed {
427422
require(!isDisputeCreated(disputeID), "Dispute already created"); // Must be empty
428423

429424
// Store dispute
430-
disputes[disputeID] = Dispute(_subgraphID, indexNode, _fisherman, _deposit);
425+
disputes[disputeID] = Dispute(attestation.subgraphID, indexNode, _fisherman, _deposit);
426+
427+
emit DisputeCreated(
428+
disputeID,
429+
attestation.subgraphID,
430+
indexNode,
431+
_fisherman,
432+
_deposit,
433+
_attestationData
434+
);
435+
}
431436

432-
emit DisputeCreated(disputeID, _subgraphID, indexNode, _fisherman, _deposit, _attestation);
437+
/**
438+
* @dev Recover the signer address of the `_attestation`
439+
* @param _attestation The attestation struct
440+
* @return Signer address
441+
*/
442+
function _recoverAttestationSigner(Attestation memory _attestation) private view returns (address) {
443+
// Obtain the hash of the fully-encoded message, per EIP-712 encoding
444+
bytes memory receipt = abi.encode(
445+
_attestation.requestCID,
446+
_attestation.responseCID,
447+
_attestation.subgraphID
448+
);
449+
bytes32 messageHash = encodeHashReceipt(receipt);
450+
451+
// Obtain the signer of the fully-encoded EIP-712 message hash
452+
// NOTE: The signer of the attestation is the indexer that served the request
453+
return _recover(messageHash, _attestation.v, _attestation.r, _attestation.s);
433454
}
434455

435456
/**
@@ -443,4 +464,83 @@ contract DisputeManager is Governed {
443464
}
444465
return id;
445466
}
467+
468+
/**
469+
* @dev Parse the bytes attestation into a struct from `_data`
470+
* @return Attestation struct
471+
*/
472+
function _parseAttestation(bytes memory _data) private pure returns (Attestation memory) {
473+
// Decode receipt
474+
(bytes32 requestCID, bytes32 responseCID, bytes32 subgraphID) = abi.decode(
475+
_data, (bytes32, bytes32, bytes32)
476+
);
477+
478+
// Decode signature
479+
// Signature is expected to be in the order defined in the Attestation struct
480+
uint8 v = _toUint8(_data, RECEIPT_SIZE_BYTES);
481+
bytes32 r = _toBytes32(_data, RECEIPT_SIZE_BYTES + 1);
482+
bytes32 s = _toBytes32(_data, RECEIPT_SIZE_BYTES + 33);
483+
484+
return Attestation(requestCID, responseCID, subgraphID, v, r, s);
485+
}
486+
487+
/**
488+
* @dev Returns the address that signed a hashed message (`hash`) with
489+
* signature `v`, `r', `s`. This address can then be used for verification purposes.
490+
* @return The address recovered from the hash and signature.
491+
*/
492+
function _recover(bytes32 _hash, uint8 _v, bytes32 _r, bytes32 _s) private pure returns (address) {
493+
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
494+
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
495+
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
496+
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
497+
//
498+
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
499+
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
500+
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
501+
// these malleable signatures as well.
502+
if (uint256(_s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
503+
revert("ECDSA: invalid signature 's' value");
504+
}
505+
506+
if (_v != 27 && _v != 28) {
507+
revert("ECDSA: invalid signature 'v' value");
508+
}
509+
510+
// If the signature is valid (and not malleable), return the signer address
511+
address signer = ecrecover(_hash, _v, _r, _s);
512+
require(signer != address(0), "ECDSA: invalid signature");
513+
514+
return signer;
515+
}
516+
517+
/**
518+
* @dev Parse a uint8 from `_bytes` starting at offset `_start`
519+
* @return uint8 value
520+
*/
521+
function _toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
522+
require(_bytes.length >= (_start + 1), "Bytes: out of bounds");
523+
uint8 tempUint;
524+
525+
assembly {
526+
tempUint := mload(add(add(_bytes, 0x1), _start))
527+
}
528+
529+
return tempUint;
530+
}
531+
532+
/**
533+
* @dev Parse a bytes32 from `_bytes` starting at offset `_start`
534+
* @return bytes32 value
535+
*/
536+
function _toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) {
537+
require(_bytes.length >= (_start + 32), "Bytes: out of bounds");
538+
bytes32 tempBytes32;
539+
540+
assembly {
541+
tempBytes32 := mload(add(add(_bytes, 0x20), _start))
542+
}
543+
544+
return tempBytes32;
545+
}
446546
}

package-lock.json

Lines changed: 3 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"web3": "^1.2.7"
2525
},
2626
"devDependencies": {
27+
"ethers": "^4.0.47",
2728
"ethlint": "^1.2.5",
2829
"husky": "^4.2.5",
2930
"ipfs-http-client": "34.0.0",

0 commit comments

Comments
 (0)