-
Notifications
You must be signed in to change notification settings - Fork 130
[PAN-2763] Add JSON-RPC API to report validator block production information #1687
[PAN-2763] Add JSON-RPC API to report validator block production information #1687
Conversation
merge from original
Pull from Main Repository
Merge from official github
Merge from master
merge from official repository
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} |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
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:
- 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.
- 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.
...main/java/tech/pegasys/pantheon/consensus/common/jsonrpc/AbstractGetSignerMetricsMethod.java
Outdated
Show resolved
Hide resolved
} else { | ||
proposersMap.put( | ||
proposerAddress, | ||
new ProposerReportBlockProductionResult(proposerAddress)); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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)) { |
There was a problem hiding this comment.
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);
}
...main/java/tech/pegasys/pantheon/consensus/common/jsonrpc/AbstractGetSignerMetricsMethod.java
Show resolved
Hide resolved
…pan-2763-validator-block-production
… report quantities as hex in the JSON output.
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.
Ok thanks you very much for your modification. I will merge your branch |
|
||
when(blockchainQueries.headBlockNumber()).thenReturn(endBlock); | ||
|
||
List<SignerMetricResult> signerMetricResultList = new java.util.ArrayList<>(); |
There was a problem hiding this comment.
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
List<SignerMetricResult> signerMetricResultList = new java.util.ArrayList<>(); | |
List<SignerMetricResult> signerMetricResultList = new ArrayList<>(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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:
Long endBlock = 3L; | |
final long endBlock = 3L; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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]
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
…on two different blocks)
merge from master
…pan-2763-validator-block-production
…kInterface implementation anyway.
There was a problem hiding this 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.
PR description
Add JSON-RPC API to report validator block production information (CLIQUE and IBFT2)