Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disputes: attestation update to latest format #188

Merged
merged 9 commits into from
May 14, 2020

Conversation

abarmat
Copy link
Contributor

@abarmat abarmat commented May 7, 2020

Related to https://github.com/graphprotocol/network-rfcs/pull/24

- 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

@abarmat abarmat changed the base branch from master to feat/curation-events-and-lang May 7, 2020 00:43
@abarmat abarmat requested review from davekaj, Jannis and That3Percent May 7, 2020 00:43
@abarmat abarmat self-assigned this May 7, 2020
@abarmat abarmat added the enhancement New feature or request label May 7, 2020
bytes32 hash;
uint16 hashFunction;
// Attestation data
struct Attestation {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was called Receipt in the latest RFC. The name of the struct factors into the hashKey of EIP-712, so we need to have consistency. I don't mind if we switch to Attestation and SignedAttestation if you prefer, but we need to choose just 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the same as in the RFC.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest here is confusing and differs from the RFC. There is an Attestation struct, which matches the RFC I think. But then, in this file there is the ATTESTATION_TYPE_HASH which holds the schema Attestation(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphID). This doesn't match the Attestation struct, but has the fields of the Receipt struct and the name of the Attestation struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@That3Percent this is the one you mention right?

struct Receipt {
  hash requestCID,
  hash responseCID,
  hash subgraphID,
}

So the type_hash says ATTESTATION when it should say RECEIPT as the type hash does not include the signature.

You are right I will do that change!

For the struct I used Attestation because that is what I parse from the incoming transaction, the receipt + signature = attestation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the type hash should be ""Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphID)" @That3Percent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that the type hash should be "Receipt(..., but also that the variable name should be renamed to RECEIPT_TYPE_HASH so as not to be confused with the Attestation struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 updated in the last commit

bytes32 s;
struct SignedAttestation {
Attestation data;
bytes sig;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also differs from the details in the latest RFC. I had copied the details from here, before these changes. So instead of bytes sig (which has variable length), we have fields for v r and s in the RFC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unpacking v,r,s into the struct now.

bytes32 r;
bytes32 s;
struct SignedAttestation {
Attestation data;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC does not nest the structs. I'm amenable to nesting, so long as it doesn't increase the size of the struct due to the padding rules. As far as I can tell, we're in the clear here since it looks like the Attestationtype takes up exactly 3 storage slots - but I'm no solidity expert so you'll want to double-check. In any case, we need to have consistency so let me know if you want me to update the RFC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated structs to match the RFC.

}

uint256 private constant ATTESTATION_SIZE_BYTES = 192;
// Includes attestation + signature (96 + 65)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is wrong (or misplaced). I think you mean to put it over PAYLOAD_SIZE_BYTES

);
bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Protocol");
bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0.1");
bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0");
Copy link

@That3Percent That3Percent May 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Sorry, GitHub wouldn't let me comment on line 174). The domainSeparator calculation seems wrong to me. Note that in the EIP domainSeparator is hashStruct(eip712Domain) but within the EIP712Domain struct the version field is of type string, not bytes32. So, pre-hashing this value is not correct. Same for the name field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string is variable length and bytes32 is fixed, I think you found an issue, I'll review it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable-length values in EIP712Domain should not be an issue, since the domainSeparator is meant to be pre-calcuated and always has an output size of bytes32.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been reviewing the EIP712 and found an example on how the eip712 domain is created. They hash all string data.

https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.sol

    function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            EIP712DOMAIN_TYPEHASH,
            keccak256(bytes(eip712Domain.name)),
            keccak256(bytes(eip712Domain.version)),
            eip712Domain.chainId,
            eip712Domain.verifyingContract
        ));
    }

In the code I'm doing:

        // EIP-712 domain separator
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                DOMAIN_TYPE_HASH,
                DOMAIN_NAME_HASH,
                DOMAIN_VERSION_HASH,
                _getChainID(),
                address(this),
                DOMAIN_SALT
            )
        );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are correct in pre-hashing the strings. I reviewed encodeData to see if there was any explanation for this and there is:

Each encoded member value is exactly 32-byte long.

...and then later...

The dynamic values bytes and string are encoded as a keccak256 hash of their contents.

require(_payload.length == PAYLOAD_SIZE_BYTES, "Signed attestation must be 161 bytes long");

// Decode attestation
bytes32 requestCID = _payload.slice(0, 32).toBytes32(0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does solidity seriously not have some utility for this? You have to do this by hand every time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solidity is super limited, we are using a BytesLib library to do bytes manipulation.
I have an item in my internal TODO to see if we can use a newer feature from Solidity 0.6 (the original code was written for Solidity 0.5.0) to slice calldata bytes. https://solidity.readthedocs.io/en/v0.6.0/types.html#array-slices

Will give quick look before we merge this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm now using a feature from Solidity 0.6 to slice arrays and removed a dependency we had on a BytesLib.

@abarmat abarmat force-pushed the feat/disputes-attestation-update branch from 9746d13 to b1bec82 Compare May 8, 2020 14:33
@abarmat abarmat force-pushed the feat/curation-events-and-lang branch from 2e7c7fc to 165852f Compare May 8, 2020 14:46
@abarmat abarmat changed the base branch from feat/curation-events-and-lang to master May 11, 2020 14:24
@abarmat abarmat force-pushed the feat/disputes-attestation-update branch from b1bec82 to a66a0f0 Compare May 11, 2020 14:43

// Decode attestation
bytes memory attestation = _data.slice(32, ATTESTATION_SIZE_BYTES);
require(attestation.length == ATTESTATION_SIZE_BYTES, "Signature must be 192 bytes long");
bytes32 requestCID = abi.decode(_data[0:32], (bytes32));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I wasn't clear on my question before about what APIs are offered by solidity. What I want to know is whether this kind of manual deserialization is necessary. Is there not some sort of transmute/cast from bytes to Attestation? Or is it always necessary for solidity developers to roll their own deserialization all the time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood your initial comment but it helped me to rewrite it in a more efficient way.

You need to roll your own deserialization, it would be great to cast from bytes to the struct directly. The closest discussion about that I found is this one ethereum/solidity#7134

_attestation.responseCID,
_attestation.subgraphID
);
bytes32 disputeID = getDisputeID(receipt);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am concerned that this uniquely identifies a dispute from only these fields, and does not include the indexNode in the hash in any way. Since multiple indexers may provide the same incorrect responses, separate disputes need to be created for them.

If anything, the disputeId should be created from r, s, and v once all other checks have passed since these uniquely identify the request/response/indexer tuple.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observation. Yes, this code is now rejecting when two receipts are similar and are submitted by different indexers. I'll ask Brandon as he had some thoughts about conflicting attestations.

_attestation.subgraphID,
_attestation.r,
_attestation.s,
_attestation.v

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field order here does not match the Attestation struct. It has v,r,s but this has r,s,v. Is that a problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use:

    struct Attestation {
        bytes32 requestCID;
        bytes32 responseCID;
        bytes32 subgraphID;
        bytes32 r;
        bytes32 s;
        uint8 v;
    }

The libraries used to sign (web3, ethers) send the signature like r,s,v

) // IEP712 : domain separator + signed attestation
receipt.substring(2) + // Attestation
messageSig.substring(2) // Signature
) // receipt + signature = attestation in EIP712 format

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is wrong. EIP712 is not a "format" for interchange. It's a method for hashing and signing - not serialization. There is an encodeStruct method, but this encoding does not match what these methods do to read/write structs from bytes. encodeStruct is not lossless, and therefore cannot in principle be used for serialization even if you wanted to. So, the string returned by this method cannot be of an attestation in "EIP712 format".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, I think I did a bad use of the language. I'd been using EIP712 "format" when it really is a standard, and I'd been calling the format the end result of encode/hashing the struct with the type_hash and each content field processed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we're on the same page. This looks like some kind of ABI encoded SignedAttestation, whereas the comment says it's EIP-712.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my comment I was referring to when I used EIP712 in the code.

About that particular section, the comment is not ok, it is just the attestation what that function returns, the receipt + signature appended.


// Message
const message = createMessage(domainSeparatorHash, attestationHash)
// Attestation signing wrapped in EIP721 format

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a typo and it says EIP721 when it meant EIP712, but to reiterate, there is no EIP-712 format.

@abarmat
Copy link
Contributor Author

abarmat commented May 13, 2020

@That3Percent I pushed changes including:

  • DisputeID considering the indexer.
  • Use ethers library (better) for signing.
  • Refactor decoding the attestation struct.

abarmat added 9 commits May 13, 2020 18:04
- 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
- 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
@abarmat abarmat force-pushed the feat/disputes-attestation-update branch from f0d8fcd to 0956b1b Compare May 13, 2020 23:45
@abarmat abarmat merged commit f8a3669 into master May 14, 2020
@abarmat abarmat deleted the feat/disputes-attestation-update branch May 14, 2020 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants