Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

[PAN-2763] Add JSON-RPC API to report validator block production information #1687

Merged
merged 27 commits into from
Jul 19, 2019
Merged

[PAN-2763] Add JSON-RPC API to report validator block production information #1687

merged 27 commits into from
Jul 19, 2019

Conversation

matkt
Copy link
Contributor

@matkt matkt commented Jul 13, 2019

PR description

Add JSON-RPC API to report validator block production information (CLIQUE and IBFT2)

  • The number of blocks from each proposer in a given block range.
  • All validators present in the last block of the range should be listed are included even if they didn’t propose a block
  • The block number of the last block proposed by each validator (if any within the given range).

@matkt
Copy link
Contributor Author

matkt commented Jul 13, 2019

Ouput

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": [
        {
            "address": "0x7ffc57839b00206d1ad20c69a1981b489f772031",
            "proposedBlockCount": "0x21",
            "lastProposedBlockNumber": "0x61"
        },
        {
            "address": "0x42eb768f2244c8811c63729a21a3569731535f06",
            "proposedBlockCount": "0x21",
            "lastProposedBlockNumber": "0x63"
        },
        {
            "address": "0xb279182d99e65703f0076e4812653aab85fca0f0",
            "proposedBlockCount": "0x21",
            "lastProposedBlockNumber": "0x62"
        }
    ]
}

Sample input

{"jsonrpc":"2.0","method":"clique_getSignerMetrics","params":["1"], "id":1}

{"jsonrpc":"2.0","method":"clique_getSignerMetrics","params":["1","1000"], "id":1}

{"jsonrpc":"2.0","method":"clique_getSignerMetrics","params":[], "id":1}

{"jsonrpc":"2.0","method":"clique_getSignerMetrics","params":["1000", "pending"], "id":1}

{"jsonrpc":"2.0","method":"clique_getSignerMetrics","params":["1000", "latest"], "id":1}

@ajsutton ajsutton self-assigned this Jul 15, 2019
Copy link
Contributor

@ajsutton ajsutton left a comment

Choose a reason for hiding this comment

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

Thanks for jumping into this - it's a really valuable contribution to add. The JIRA ticket isn't particularly polished or well thought out though. It was added as a bit of a place holder so apologies for where it's unclear or if we wind up having to change requirements a bit. Very pleased to have you working on it and I'll do my best to work with you to clarify anything and get this in.

You probably know a lot of what I cover below, but I'll include it all so hopefully we wind up with the same understanding.

In the JIRA ticket, the terms proposer and validator are being used a bit loosely. When using PoA like Clique or IBFT, for any given block there is a set of validators represented by an Address. That set of validators can be retrieved from the VoteTallyCache with voteTallyCache.getVoteTallyAfterBlock(header).getValidators().

For every block there is also a single proposer which is the validator that actually selected the list of transactions to include and created the proposed block. For Clique the proposer does this alone, for IBFT it then goes through a further process to get enough other validators to also sign the block but in both cases there's still only the one proposer.

What we ultimately want to get out of this JSON-RPC method is a report showing which validators are proposing blocks so we can identify validators that have gone offline and either fix them or vote them out. So very roughly something like:

"proposers": [
  { "address":  "0x7ffc57839b00206d1ad20c69a1981b489f772031", "proposedBlockCount": 315, "lastProposedBlockNumber": 123456 },
  { "address": "0x42eb768f2244c8811c63729a21a3569731535f06", "proposedBlockCount": 314, "lastProposedBlockNumber": 123455 },
  { "address": "0xb279182d99e65703f0076e4812653aab85fca0f0", "proposedBlockCount": 0 }
}

The part about including all validators in the last block being included in the output is so that we get that last entry for 0xb279182d99e65703f0076e4812653aab85fca0f0. They didn't propose a block at all in the period we looked at so would otherwise be omitted but get explicitly included because they're still in the list of validators at the end of the range.

return startBlock < endBlock;
}

protected abstract List<Address> getProposersOfBlock(final BlockHeader header);
Copy link
Contributor

Choose a reason for hiding this comment

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

There is only ever one proposer of a block - it's the validator that selects the transactions and creates the proposed block, even if multiple validators are later involved in signing off on that block. And you can use the tech.pegasys.pantheon.consensus.common.BlockInterface.getProposerOfBlock method to get that validator without needing to worry about which consensus algorithm is currently in use. You can create the appropriate implementation (either CliqueBlockInterface or IbftBlockInterface) in the (Clique/Ibft)JsonRpcMethodsFactory.

Hopefully that simplifies things a fair bit so the changes should just be isolated to the actual JSON-RPC classes.


final Map<Address, ProposerReportBlockProductionResult> proposersMap = new HashMap<>();
final Map<Address, ValidatorReportBlockProductionResult> validatorInLastBlockMap =
new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Hopefully the clarifications in my overall comment help to make sense of this but we'd only need the one map here. The basic approach would be:

  1. Iterate through each block in the range and increment the count of number of proposed blocks for the proposer of that block. Note you might still get some proposers who don't yet exist in the map if they've been voted out before the end of the range.
  2. For each validator in the last block of the range not already in proposerMap, add it with 0 proposed blocks. That will ensure they're all present in the report even if none ever propose blocks.

} else {
proposersMap.put(
proposerAddress,
new ProposerReportBlockProductionResult(proposerAddress));
Copy link
Contributor

Choose a reason for hiding this comment

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

The Map.computeIfAbsent is really helpful here. If ProposerReportBlockProductionResult defaulted to 0 blocks proposed instead of 1, then you could just use proposersMap.computeIfAbsent(proposerAddress, ProposerReportBlockProductionResult::new).incrementeNbBlock();

public class ProposerReportBlockProductionResult {

public final String address;
public long nbBlockProposed;
Copy link
Contributor

Choose a reason for hiding this comment

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

We generally try to avoid abbreviations so something like proposedBlockCount would be a better name.

public JsonRpcResponse response(final JsonRpcRequest request) {

final long startBlock = parameters.required(request.getParams(), 0, Long.class);
final long endBlock = parameters.required(request.getParams(), 1, Long.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should support the usual values for block parameters like latest. parameters().required(request.getParams(), 0, BlockParameter.class); will handle the parsing of that. pending and latest would mean the same thing (current chain head) in this context.

I'd probably make both arguments optional as well. Start would default to 100 blocks before current chain head and end defaults to chain head. That way you can call with no arguments and get a useful result (most of the time you want to know which of the current validators hasn't proposed recently), or specify a starting block to get from their to chain head easily, or both start and end for a custom range.

Copy link
Contributor

@ajsutton ajsutton left a comment

Choose a reason for hiding this comment

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

Looking really good. Just a couple of tweaks and we're there I think. Thanks so much for your work on this.

final long fromBlockNumber = getFromBlockNumber(startBlockParameter);
final long toBlockNumber = getEndBlockNumber(endBlockParameter);

if (isValidParameters(fromBlockNumber, toBlockNumber)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We typically exit early in cases like this, so

if (!isValidParameters(fromBlockNumber, toBlockNubmer)) {
  return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS);
}

@ajsutton
Copy link
Contributor

I realised we should be reporting the values in the JSON response as hex (to match all the other quantity values). This is unfortunately a lot less readable but just how JSON-RPC works.

ajsutton@0bd0892 applies the couple of suggestions I made above, plus the change to use hex. It's on the branch https://github.com/ajsutton/pantheon/commits/validator-metrics if it's easier to just pull that in.

Exit early, handle block parameter aliases (earliest, latest etc) and report quantities as hex in the JSON output.
@matkt
Copy link
Contributor Author

matkt commented Jul 17, 2019

I realised we should be reporting the values in the JSON response as hex (to match all the other quantity values). This is unfortunately a lot less readable but just how JSON-RPC works.

ajsutton@0bd0892 applies the couple of suggestions I made above, plus the change to use hex. It's on the branch https://github.com/ajsutton/pantheon/commits/validator-metrics if it's easier to just pull that in.

Ok thanks you very much for your modification. I will merge your branch

@matkt matkt marked this pull request as ready for review July 17, 2019 22:19

when(blockchainQueries.headBlockNumber()).thenReturn(endBlock);

List<SignerMetricResult> signerMetricResultList = new java.util.ArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Shouldn't don't need the full package name here

Suggested change
List<SignerMetricResult> signerMetricResultList = new java.util.ArrayList<>();
List<SignerMetricResult> signerMetricResultList = new ArrayList<>();

Copy link
Contributor

Choose a reason for hiding this comment

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

Also for the other new java.util.ArrayList() calls below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes an error that I had not seen. I correct that

public void getSignerMetricsWhenNoParams() {

Long startBlock = 1L;
Long endBlock = 3L;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: These should just be primitives and we add the final keyword whenever possible in Pantheon (well try to - we're not as consistent as we should be). So:

Suggested change
Long endBlock = 3L;
final long endBlock = 3L;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

LongStream.range(startBlock, endBlock)
.forEach(value -> verify(blockchainQueries).getBlockHeaderByNumber(value));

verify(blockchainQueries, atLeast(2)).headBlockNumber();
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think there's any reason to assert these class to blockchainQueries - they're just implementation details that it needs to get the information. We just check it comes up with the right answer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is removed


signerMetricResultList.forEach(
signerMetricResult ->
assertThat(response.getResult().toString()).contains(signerMetricResult.toString()));
Copy link
Contributor

Choose a reason for hiding this comment

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

These assertions should actually be stronger - we expect that the result only contains the expected signers and the order isn't important (because the list of stats is built off of a HashMap the order isn't guaranteed). So:

    assertThat((Collection<SignerMetricResult>) response.getResult())
        .containsExactlyInAnyOrderElementsOf(signerMetricResultList);

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 wanted to try this proposal but I have an error during the pipeline "unchecked cast assertThat ((Collection ) response.getResult ()) "that's why I used another way

Copy link
Contributor Author

@matkt matkt Jul 18, 2019

Choose a reason for hiding this comment

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

I add the @SuppressWarnings("unchecked") annotation


private SignerMetricResult generateBlock(final long number) {
final SECP256K1.KeyPair proposerKeyPair = SECP256K1.KeyPair.generate();
final Address proposerAddressBlock = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
Copy link
Contributor

Choose a reason for hiding this comment

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

Creating keypairs is fairly expensive and we really want to test that the number of blocks gets incremented so it's probably worth creating a few key pairs as static fields which are just iterated through here (e.g. final SECP256K1.KeyPair proposerKeyPair = KEYPAIRS[number % KEYPAIRS.length])

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

@ajsutton ajsutton left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for picking this up.

I realised during the final review that because we're using a mock for the BlockInterface we can simplify the tests to use Address directly and not need to create real private keys or properly signed blocks. So I've simplified that and applied a few final keywords.

All good to go.

@ajsutton ajsutton merged commit 3ab368d into PegaSysEng:master Jul 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
2 participants