Skip to main content

LCP Client

LCP Client is a on-chain light client that implements ICS-02, which verifies commitments generated by ELC. It verifies a Verifiable Quote generated by LCP and retrieves and stores the Enclave Key contained. Initially, it supports IAS AVR as a Verifiable Quote.

The client has its state as ClientState and ConsensusState. Each definition is as follows:

message ClientState {
ibc.core.client.v1.Height latest_height = 1;
bytes mrenclave = 2;
uint64 key_expiration = 3;
repeated bytes attested_keys = 4;
}

message ConsensusState {
bytes state_id = 1;
uint64 timestamp = 2;
}

The ClientState consists of a list of public Enclave keys that verify the MRENCLAVE of the LCP Enclave and ELC proof and the expiration period of each Enclave key. The list of keys is added after being verified by the RegisterEnclaveKey command described later.

ConsensusState is the state of the ELC for each Upstream height. It tracks the ELC client's StateID and the timestamp of the upstream associated with the height.

The LCP client initializes with three commands: CreateClient, RegisterEnclaveKey, and UpdateClient. After initialization, the client can perform state verification based on its state. The following figure shows the workflow for the initialization.

CreateClient

CreateClient is a command to initialize the LCP client state by specifying the MRENCLAVE of the LCP Enclave and the Enclave Key expiration period. MRENCLAVE is for AVR validation required to register the key with RegisterEnclaveKey. The expiration period is specified in seconds.

RegisterEnclaveKey

RegisterEnclaveKey is a command to register the Enclave key generated by LCP Enclave to the client. The command contains the Verifiable Quote (AVR and its signature in IAS) obtained by the Attestation Service. The definition of the command and the corresponding function are as follows:

struct RegisterEnclaveKeyCommand {
pub avr_body: Vec<u8>,
pub signature: Vec<u8>,
}

fn register_enclave_key(
&self,
ctx: &dyn ClientReader,
client_id: ClientId,
client_state: ClientState,
command: RegisterEnclaveKeyCommand,
) -> Result<(ClientState, ConsensusState), Ics02Error> {
let vctx = self.validation_context(ctx);
let avr = parse_avr(command.avr_body);
assert!(verify_report(&vctx, &client_state, &avr, &header.signature));
let key = read_enclave_key_from_report(&avr).unwrap();
let expiration = avr.quote.timestamp + client_state.key_expiration;
let any_consensus_state = ctx
.consensus_state(&client_id, client_state.latest_height)
.unwrap();
let consensus_state = ConsensusState::try_from(any_consensus_state)?;
let new_client_state = client_state.with_new_key((key, expiration));

Ok((new_client_state, consensus_state))
}

This function verifies the given AVR of an LCP Enclave and sets the public Enclave key contained in the Report to the ClientState with an expiration date as the approved key.

The verification depends on the Attestation Service used; in the case of IAS, it verifies the following two things:

  1. Verification of the signature by Intel on the AVR
  2. Validation of the TCB (Trusted Computing Base) of the Enclave and CPU

For #1, the AVR is signed by the IAS Report Signing Key, which is verified by the following steps:

  1. Decode and verify the Report Signing Certificate Chain that was sent together with the report (see Report Signing Certificate Chain for details). Verify that the chain is rooted in a trusted Attestation Report Signing CA Certificate (available to download upon successful registration to IAS).
  2. Optionally, verify that the certificates in the chain have not been revoked (using CRLs indicated in the certificates).
  3. Verify the signature over the report using Attestation Report Signing Certificate.

(These are quoted from https://api.trustedservices.intel.com/documents/sgx-attestation-api-spec.pdf)

Regarding #2, the validator can reject key registration based on the value of isvEnclaveQuoteStatus or advisoryID contained in the AVR.This validation rule is implemented in the LCP Client based on LCP's security assumptions.

In addition, the validated key has an expiration date equal to the timestamp of the AVR plus the key_expiration of the ClientState. Thus, this ensures that the validity of each status updated by the IAS is verified within the key_expiration.

UpdateClient

The UpdateClient command is used to update the client state. It includes ELC's UpdateClient Commitment, a signature with the Enclave key, and its public key. The command definition and the corresponding function are as follows:

pub struct UpdateClientCommand {
pub commitment_bytes: Vec<u8>,
pub signer: Vec<u8>,
pub signature: Vec<u8>,
pub commitment: UpdateClientCommitment,
}

fn update_client(
&self,
ctx: &dyn ClientReader,
client_id: ClientId,
client_state: ClientState,
command: UpdateClientCommand,
) -> Result<(ClientState, ConsensusState), Ics02Error> {

if client_state.latest_height.is_zero() {
// if the client is not initialized, `prev_height` and `prev_state_id` must be empty
assert!(command.prev_height().is_none() && command.prev_state_id().is_none());
} else {
// if the client is initialized, `prev_height` and `prev_state_id` must not be empty.
assert!(command.prev_height().is_some() && command.prev_state_id().is_some());
}


// check if the proxy's trusted consensus state exists in the store
let prev_consensus_state: ConsensusState = ctx
.consensus_state(&client_id, command.prev_height().unwrap())?
.try_into()?;
assert!(prev_consensus_state.state_id == command.prev_state_id().unwrap());

// check if the specified signer exists in the client state
assert!(client_state.contains(&command.signer()));

// check if the specified signer is not expired
assert!(client_state.is_available_key(ctx, &header.signer()));

// check if the `command.signer` matches the commitment prover
let signer = verify_signature(&command.commitment_bytes, &command.signature).unwrap();
assert!(command.signer() == signer);

// check if LCP's validation context matches our context
let vctx = self.validation_context(ctx);
assert!(validation_predicate(&vctx, command.validation_params()).unwrap());

// create a new state
let new_client_state = client_state.with_command(&command);
let new_consensus_state = ConsensusState {
state_id: command.state_id(),
timestamp: command.timestamp_as_u128(),
};

Ok((new_client_state, new_consensus_state))
}

The function verifies the UpdateClient commitment and signature by the keys in the attested_keys of the specified ClientState. The commitment in the first UpdateClient command executed following CreateClient must be the one generated by initializeClient by ELC's client.

It verifies with the following steps:

  1. Verifies that the specified public enclave key exists in ClientState
  2. Verifies that the key has not expired
  3. Verifies that the the commitment proof is signed by the key
  4. Evaluates the predicate for the validity of the ELC validation results contained in the commitment

On successful verification, a ConsensusState is created based on the state_id in the commitment and associated Upstream timestamp and height.

Finally, the PacketRelay flow is shown in the figure below:

PacketRelay

PacketRelay is a command that verifies a Packet commitment by the LCP client. The command includes a State Commitment that indicates the result of verifying the Packet commitment of Upstream generated by ELC. The function corresponding to the command is as follows:

pub fn verify_packet_data(
&self,
ctx: &dyn ChannelReader,
client_state: &ClientState,
height: Height,
connection_end: &ConnectionEnd,
proof: &CommitmentProofBytes,
root: &CommitmentRoot,
port_id: &PortId,
channel_id: &ChannelId,
sequence: Sequence,
packet_commitment: String,
) -> Result<(), Ics02Error> {
// convert `proof` to StateCommitmentProof
let commitment_proof = Self::convert_to_state_commitment_proof(proof).unwrap();
let commitment = commitment_proof.commitment();

assert!(height == commitment.height);

// check if `.path` matches expected path
assert!(
commitment.path
== CommitmentsPath {
port_id: port_id.clone(),
channel_id: channel_id.clone(),
sequence: sequence,
}
.into()
);

// check if the specified signer exists in the client state
assert!(client_state.contains(&commitment_proof.signer));

// check if the specified signer is not expired
assert!(client_state.is_available_key(ctx, &commitment_proof.signer));

// verify the signature with `commitment_proof.signer`
let signer = verify_signature(
&commitment_proof.commitment_bytes,
&commitment_proof.signature,
)
.unwrap();
assert!(Address::from(&commitment_proof.signer as &[u8]) == signer);

let mut packet_commitment_bytes = Vec::new();
packet_commitment
.encode(&mut packet_commitment_bytes)
.expect("buffer size too small");

// check if `.value` matches expected state
assert!(packet_commitment_bytes == commitment.value);

let channel_end = ctx.channel_end(&(port_id.clone(), channel_id.clone())).unwrap();
let connection_end = ctx.connection_end(&channel_end.connection_hops[0]).unwrap();

// check if `.state_id` matches the corresponding stored consensus state's state_id
let consensus_state = ConsensusState::try_from(ctx.client_consensus_state(connection_end.client_id(), height).unwrap())?;
assert!(consensus_state.state_id == commitment.state_id);

Ok(())
}

For the validity of the State Commitment, which indicates the status of the packet, the client verifies that:

  1. The ClientState includes the signer of the proof
  2. The signer has not expired
  3. The signer verified commitment.signature
  4. state_id of the commitment matches that of the ConsensusState