- 2022-06-10: Created the ADR.
Tendermint provides an interface ("privval") for external processes
that validators deploy with different signing backends (e.g. YubiHSM).
In Tendermint 0.35 and beyond, the signer process is expected to be a gRPC server/service
that Tendermint connects to. Using tonic-build
, the gRPC server interface
code can be generated as an extension to the existing tendermint-proto
crate definitions.
The signing backend implementor could then possibly implement this interface
from tendermint-proto
as follows:
#[tonic::async_trait]
impl PrivValidatorApi for ValidatorSigningBackend
{
async fn get_pub_key(
&self,
request: Request<RawPubKeyRequest>,
) -> Result<Response<RawPubKeyResponse>, Status> {
/// ...
}
async fn sign_vote(
&self,
request: Request<RawSignVoteRequest>,
) -> Result<Response<RawSignedVoteResponse>, Status> {
// ...
}
async fn sign_proposal(
&self,
request: Request<RawSignProposalRequest>,
) -> Result<Response<RawSignedProposalResponse>, Status> {
// ...
}
}
This implementation, however, needs to use the raw protobuf types, so each implementor would need to duplicate the same common validation code.
The tendermint-validator
crate can be added to the tendermint-rs repository
that will help the developers with implementing Tendermint validator signing backends.
It will provide basic tonic's Server
options (e.g. TLS) and the implementation of PrivValidatorApi
. This implementation
can accept different signing backends that are implemented via two traits:
- the signing one (that e.g. communicates with HSM):
#[tonic::async_trait]
pub trait SignerProvider {
type E: std::error::Error;
async fn sign(&self, signable_bytes: &[u8]) -> Result<Signature, Self::E>;
async fn load_pubkey(&self) -> Result<PublicKey, Self::E>;
}
- the state storage one that persists the validator state (in a file or whatever makes sense in the signing context, e.g. write to CPU monotonic counters):
#[tonic::async_trait]
pub trait ValidatorStateProvider {
type E: std::error::Error;
async fn load_state(&self) -> Result<consensus::State, Self::E>;
async fn persist_state(&mut self, new_state: &consensus::State) -> Result<(), Self::E>;
}
Besides checking the chain ids from requests and calling corresponding providers, the implementation can also do the following common validation steps:
- use the existing domain types from the
tendermint
crate to validate the incoming requests; - check the signing requests' heights against the optional maximum;
- check the signing requests against the last validator state and persist the new states after successful signing operations.
For the validator state checking, the existing consensus State
can be extended to have
conversions from the signing requests as well as
PartialOrd
implementation.
The crate can also contain an example "software signer" backend implementation of the provider traits that stores the signing key in-memory and uses text files for the validator state persistence.
Accepted
- Reduced code duplication
- Easier implementation of new signing backends
- The need to use async functions due to
tonic
's gRPC implementation
- One more crate