cip40
titlePrivval Interface Extension for Arbitrary Message Signing
descriptionExtends the CometBFT privval interface to support signing arbitrary messages for offchain protocols.
authorCHAMI Rachid (@rach-id), Evan Forbes (evan-forbes)
discussions-tohttps://forum.celestia.org/t/cip-40-privval-interface-extension-for-arbitrary-message-signing/2102
statusFinal
typeStandards Track
categoryInterface
created2025-07-25

Abstract

This CIP proposes extending the CometBFT privval.Message interface to support signing arbitrary messages. This enhancement enables validators to use their consensus keys and existing Key Management Systems (KMS) to sign offchain protocol messages, facilitating the implementation of high-throughput and low-latency gossiping protocols while maintaining backwards compatibility.

Motivation

Celestia is implementing advanced gossiping protocols that require validators to commit to offchain messages using their consensus keys. These protocols include the Full Mesh Overlay (FMO), where validators sign to prove their identity to peers, and Vacuum!, where validators commit to having blobs before block inclusion.

Current KMS implementations only support signing predefined consensus messages through the existing privval.Message interface. This limitation prevents adding other mechanisms that rely on validator signatures for offchain message authentication.

The proposed extension addresses this gap by allowing validators to sign arbitrary messages while maintaining the security properties of their existing key management infrastructure.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Protocol Buffer Definitions

The following message types SHALL be added to the privval.Message interface:

message SignRawBytesRequest {
  string chain_id  = 1;
  bytes  raw_bytes = 2;
  string unique_id = 3;
}

message SignedRawBytesResponse {
  bytes             signature = 1;
  RemoteSignerError error     = 2;
}

Interface Extension

The privval.Message interface SHALL be extended to include the new message types:

message Message {
  oneof sum {
    PubKeyRequest            pub_key_request             = 1;
    PubKeyResponse           pub_key_response            = 2;
    SignVoteRequest          sign_vote_request           = 3;
    SignedVoteResponse       signed_vote_response        = 4;
    SignProposalRequest      sign_proposal_request       = 5;
    SignedProposalResponse   signed_proposal_response    = 6;
    PingRequest              ping_request                = 7;
    PingResponse             ping_response               = 8;
+    SignRawMessageRequest    sign_raw_message_request    = 9;
+    SignedRawMessageResponse signed_raw_message_response = 10;
  }
}

Field Specifications

  • chain_id: The chain identifier to prevent cross-chain signature reuse. It’s required as it’s used in signing and also routing in KMS implementation.
  • raw_bytes: It’s the data that needs to be signed over. Worth noting that this shouldn’t be a digest, it needs to be the actual data, and it’s a required field. The sign bytes are constructed as defined in the sign bytes construction section.
  • unique_id: A required string identifier for the specific protocol or message type being signed.
  • signature: The resulting signature bytes from the signing operation.
  • error: Error information if the signing operation fails.

Sign Bytes Construction

The actual bytes that are signed MUST be constructed by concatenating the domain separator "COMET::RAW_BYTES::SIGN" with the protobuf encoding of the SignRawBytesRequest:

// RawBytesSignBytesPrefix defines a domain separator prefix added to raw bytes to ensure the resulting
// signed message can't be confused with a consensus message, which could lead to double signing
const RawBytesSignBytesPrefix = "COMET::RAW_BYTES::SIGN"

// RawBytesMessageSignBytes returns the canonical bytes for signing raw data messages.
// It requires non-empty chainID, uniqueID, and rawBytes to prevent security issues.
// Returns error if any required parameter is empty or if marshaling fails.
func RawBytesMessageSignBytes(chainID, uniqueID string, rawBytes []byte) ([]byte, error) {
	if chainID == "" {
		return nil, errors.New("chainID cannot be empty")
	}

	if uniqueID == "" {
		return nil, fmt.Errorf("uniqueID cannot be empty")
	}

	if len(rawBytes) == 0 {
		return nil, fmt.Errorf("rawBytes cannot be empty")
	}

	prefix := []byte(RawBytesSignBytesPrefix)

	signRequest := &privval.SignRawBytesRequest{
		ChainId:  chainID,
		RawBytes: rawBytes,
		UniqueId: uniqueID,
	}
	protoBytes, err := protoio.MarshalDelimited(signRequest)
	if err != nil {
		return nil, err
	}
	return append(prefix, protoBytes...), nil
}

Implementation Requirements

  1. KMS implementations MUST support the new message types for full compatibility.
  2. The signing operation MUST use the same cryptographic key as consensus message signing.
  3. The chain_id field MUST match the configured chain identifier.
  4. Double-signing protection is NOT REQUIRED for raw message signing operations.
  5. Generate the sign bytes as per the sign bytes construction section.
  6. Sign the resulting byte array.

Rationale

Design Decisions

Chain ID Inclusion: Including the chain_id prevents signature reuse across different networks.

Unique ID Field: The required unique_id field allows protocols to differentiate between different message types or contexts without requiring separate interface extensions.

No Double-Signing Protection: Unlike consensus messages, raw messages do not require double-signing protection as they do not affect chain safety.

Alternative Approaches Considered

Separate Interface: Creating a completely separate interface was considered but rejected to maintain consistency with existing KMS integrations and avoid fragmenting the signing infrastructure.

Backwards Compatibility

This proposal is fully backwards compatible. Existing KMS implementations will continue to function normally, as the new message types use previously unused field numbers in the protobuf oneof union. That being said, all repos that import the interface MUST update their implementations to at least use a noop.

Security Considerations

Signature Verification

Applications using this interface MUST implement proper signature verification including:

  • Validating the signer’s public key against the validator set
  • Verifying the chain_id matches the expected network
  • Confirming the sign bytes (raw_message) are correct, likely by generating the raw message itself.

Reference Implementation

A reference implementation is available in CometBFT PR #5138. This implementation has been tested on multiple private testnets and the Mamo testnet, demonstrating practical viability.

A draft implementation for the Horcrux KMS is also available, showing integration patterns for existing key management systems. #306

Copyright and related rights waived via CC0.