Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Concordium account key files
*.export

target
dist
.eslintcache
Expand Down
77 changes: 77 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions credential-verification-service/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Unreleased

- Added account nonce management to the service.
- Added the logic of the `/verifiable-presentations/create-verification-request` api endpoint flow. This endpoint submits the `verification-request-anchor (VRA)` on-chain.
- Initial service.
5 changes: 5 additions & 0 deletions credential-verification-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ tokio-util.workspace = true
prometheus-client.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tonic = {workspace = true, features = ["tls", "tls-roots"]}
thiserror.workspace = true
tower-http = {workspace = true, features = [
"compression-zstd",
] }

[[bin]]
name = "credential-verification-service"
Expand Down
31 changes: 28 additions & 3 deletions credential-verification-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,38 @@ docker run --rm \
credential-verification-service
```


you should then be able to curl the health endpoint from outside the container, for example:
You should then be able to curl the health endpoint from outside the container, for example:

`curl http://localhost:8001/health`

## Build the service from the source code

You can build the serive locally as follows:

```
cargo build
```

## Run the servie from the source code

You can run the serive locally as follows:

```
cargo run -- --node-endpoint https://grpc.testnet.concordium.com:20000 --account 4bbdAUCDK2D6cUvUeprGr4FaSaHXKuYmYVjyCa4bXSCu3NUXzA.export
```

## Configuration options

The following options are supported:

- `--node-endpoint [env: CREDENTIAL_VERIFICATION_SERVICE_NODE_GRPC_ENDPOINT]`: the URL of the node's GRPC V2 interface, e.g., http://node.testnet.concordium.com:20000
- `--request-timeout [env: CREDENTIAL_VERIFICATION_SERVICE_REQUEST_TIMEOUT]`: The request timeout for a request to be processed with the credential service api in milliseconds (defaults to 15 seconds if not given).
- `--grpc-node-request-timeout [env: CREDENTIAL_VERIFICATION_GRPC_NODE_REQUEST_TIMEOUT]`: The request timeout to the Concordium node in milliseconds (defaults to 1 second if not given).
- `--log-level [env: CREDENTIAL_VERIFICATION_SERVICE_LOG_LEVEL]`: The log level (defaults to info if not given).
- `--account [env: CREDENTIAL_VERIFICATION_SERVICE_ACCOUNT]`: The path to the account key file.
- `--api-address [env: CREDENTIAL_VERIFICATION_SERVICE_API_ADDRESS]`: The socket address where the service exposes its API (defaults to `127.0.0.1:8000` if not given).
- `--monitoring-address [env: CREDENTIAL_VERIFICATION_SERVICE_MONITORING_ADDRESS]`: The socket address used for health and metrics monitoring (defaults to `127.0.0.1:8001` if not given).
- `--transaction-expiry [env: CREDENTIAL_VERIFICATION_SERVICE_TRANSACTION_EXPIRY]`: The number of seconds in the future when the anchor transactions should expiry (defaults to 15 seconds if not given).

## API Documentation

Expand Down Expand Up @@ -67,4 +93,3 @@ Diagrams and Sample Payloads:

## Architecture
- 🗺️ [Architecture Overview](docs/architecture.md)

22 changes: 17 additions & 5 deletions credential-verification-service/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
use axum::{Router, routing::get};
use crate::types::Service;
use axum::{
Router,
routing::{get, post},
};
use prometheus_client::registry::Registry;
use std::sync::Arc;

use crate::service::Service;

mod monitoring;
mod verification_request;
mod verifier;

/// Router exposing the service's endpoints
pub fn router(service: Arc<Service>) -> Router {
pub fn router(service: Arc<Service>, request_timeout: u64) -> Router {
Router::new()
.route("/verify", get(verifier::verify))
.route("/verifiable-presentations/verify", post(verifier::verify))
.route(
"/verifiable-presentations/create-verification-request",
post(verification_request::create_verification_request),
)
.with_state(service)
.layer(tower_http::timeout::TimeoutLayer::new(
std::time::Duration::from_millis(request_timeout),
))
.layer(tower_http::limit::RequestBodyLimitLayer::new(1_000_000)) // at most 1000kB of data.
.layer(tower_http::compression::CompressionLayer::new())
}

/// Router exposing the Prometheus metrics and health endpoint.
Expand Down
127 changes: 127 additions & 0 deletions credential-verification-service/src/api/verification_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//! Handler for create-verification-request endpoint.
use crate::{
api_types::CreateVerificationRequest,
types::{ServerError, Service},
};
use axum::{Json, extract::State};
use concordium_rust_sdk::{
base::web3id::v1::anchor::{
LabeledContextProperty, UnfilledContextInformationBuilder, VerificationRequest,
VerificationRequestDataBuilder,
},
common::types::TransactionTime,
v2::{QueryError, RPCError},
web3id::v1::{
AnchorTransactionMetadata, CreateAnchorError::Query,
create_verification_request_and_submit_request_anchor,
},
};
use std::sync::Arc;

pub async fn create_verification_request(
State(state): State<Arc<Service>>,
Json(params): Json<CreateVerificationRequest>,
) -> Result<Json<VerificationRequest>, ServerError> {
let context = UnfilledContextInformationBuilder::new_simple(
params.nonce,
params.connection_id,
params.context_string,
)
.given(LabeledContextProperty::ResourceId(params.rescource_id))
.build();

let mut builder = VerificationRequestDataBuilder::new(context);
for claim in params.requested_claims {
builder = builder.subject_claim(claim);
}
let verification_request_data = builder.build();

// Transaction should expiry after some seconds.
let expiry = TransactionTime::seconds_after(state.transaction_expiry_secs);

let mut node_client = state.node_client.clone();

// Get the current nonce for the backend wallet and lock it. This is necessary
// since it is possible that API requests come in parallel. The nonce is
// increased by 1 and its lock is released after the transaction is submitted to
// the blockchain.
let mut account_sequence_number = state.nonce.lock().await;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need a timeout on this async operation (locking), like we have for calling the node.


let anchor_transaction_metadata = AnchorTransactionMetadata {
signer: &state.account_keys,
sender: state.account_keys.address,
account_sequence_number: *account_sequence_number,
expiry,
};

let verification_request = create_verification_request_and_submit_request_anchor(
&mut node_client,
anchor_transaction_metadata,
verification_request_data.clone(),
None,
)
.await;

match verification_request {
Ok(req) => {
// If the submission of the anchor transaction was successful,
// increase the account_sequence_number tracked in this service.
*account_sequence_number = account_sequence_number.next();
Ok(Json(req))
}

Err(e) => {
// If the error is due to an account sequence number mismatch,
// refresh the value in the state and try to resubmit the transaction.
if let Query(QueryError::RPCError(RPCError::CallError(ref err))) = e {
let msg = err.message();
let is_nonce_err = msg == "Duplicate nonce" || msg == "Nonce too large";

if is_nonce_err {
tracing::warn!(
"Unable to submit transaction on-chain successfully due to account nonce mismatch: {}.
Account nonce will be re-freshed and transaction will be re-submitted.",
msg
);

// Refresh nonce
let nonce_response = node_client
.get_next_account_sequence_number(&state.account_keys.address)
.await
.map_err(|e| ServerError::SubmitAnchorTransaction(e.into()))?;

*account_sequence_number = nonce_response.nonce;

tracing::info!("Refreshed account nonce successfully.");

// Retry anchor transaction.
let meta = AnchorTransactionMetadata {
signer: &state.account_keys,
sender: state.account_keys.address,
account_sequence_number: nonce_response.nonce,
expiry,
};

let verification_request =
create_verification_request_and_submit_request_anchor(
&mut node_client,
meta,
verification_request_data,
None,
)
.await?;

tracing::info!(
"Successfully submitted anchor transaction after the account nonce was refreshed."
);

*account_sequence_number = account_sequence_number.next();

return Ok(Json(verification_request));
}
}

Err(ServerError::SubmitAnchorTransaction(e))
}
}
}
9 changes: 6 additions & 3 deletions credential-verification-service/src/api/verifier.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Handlers for verification endpoints.
//! Handler for the verification endpoints.
use crate::types::Service;
use axum::{Json, extract::State};
use std::sync::Arc;

pub async fn verify() -> Result<String, String> {
Ok("Verified".to_owned())
pub async fn verify(_state: State<Arc<Service>>, Json(_payload): Json<bool>) -> Json<String> {
Json("ok".to_string())
}
Loading