Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for handling billing process for the service offered by the operator #18

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

Ayush-Yadav
Copy link
Member

Add folllowing functionalities to the serverless backend :-

  1. Interacting with the serverless contract to make sure the deposit corresponding to a tx_hash or code is above a minimum threshold
  2. Tracking the time taken to process the request and generate the response, in order to calculate the service fee to be charged by the operator
  3. Sending transactions to the serverless contract , at regular intervals in the background, for verifying the signature and deducting the fees from the deposits and paying the sum to the operator

Copy link
Member

@vg-27 vg-27 left a comment

Choose a reason for hiding this comment

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

Operator key can't be used inside enclave. Think, how will it be configured? Using startup script in docker image. And that startup script is included as part of calculating PCRs, implies static contents of enclaves are public, implies operator key is public, which is obviously not possible. Need to think of another way. Also comment if my understanding is wrong. cc: @roshanr95

src/billing_job.rs Outdated Show resolved Hide resolved
src/billing_job.rs Outdated Show resolved Hide resolved
src/workerd.rs Outdated Show resolved Hide resolved
@Ayush-Yadav
Copy link
Member Author

Operator key can't be used inside enclave. Think, how will it be configured? Using startup script in docker image. And that startup script is included as part of calculating PCRs, implies static contents of enclaves are public, implies operator key is public, which is obviously not possible. Need to think of another way. Also comment if my understanding is wrong. cc: @roshanr95

We'll need the operator's private key in some way to be able to send signed billing transactions using their wallet. One way can be to ask them to store their private key in their system and allow the docker image or the backend to fetch it from there. Not sure if this will be very helpful and can increase the complexity. @roshanr95 Let us discuss this.

src/billing_job.rs Outdated Show resolved Hide resolved
src/billing_job.rs Outdated Show resolved Hide resolved
src/billing_job.rs Outdated Show resolved Hide resolved
src/handler.rs Outdated Show resolved Hide resolved
src/tests.rs Outdated Show resolved Hide resolved
src/workerd.rs Outdated Show resolved Hide resolved
Copy link
Member

@vg-27 vg-27 left a comment

Choose a reason for hiding this comment

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

Looks good, except the usage of operator wallet key inside of enclave. So, finalise upon that.

src/billing_job.rs Outdated Show resolved Hide resolved
@Ayush-Yadav
Copy link
Member Author

Looks good, except the usage of operator wallet key inside of enclave. So, finalise upon that.

Sure, @roshanr95 is looking into it.

@roshanr95
Copy link
Member

@Ayush-Yadav Let's pull it outside I guess. There's no easy way to feed the enclave with the operator key and whatever we end up doing is going to be useless once we pull it outside, might as well do it now itself.

@roshanr95
Copy link
Member

roshanr95 commented Jan 30, 2024

Also, cargo fmt is your friend for all rust formatting stuff, run it before commits or set your editor to run it on save.

@Ayush-Yadav Ayush-Yadav force-pushed the ayush/billing branch 2 times, most recently from 196716e to 5b1cc2d Compare February 7, 2024 06:12
Copy link
Member

@vg-27 vg-27 left a comment

Choose a reason for hiding this comment

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

Rest looks good to me.

src/contract_abi.json Outdated Show resolved Hide resolved
src/workerd.rs Outdated Show resolved Hide resolved
src/workerd.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
src/workerd.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
Copy link
Member

@vg-27 vg-27 left a comment

Choose a reason for hiding this comment

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

What's the design idea behind separating these two calls?

src/billing_handler.rs Outdated Show resolved Hide resolved
@Ayush-Yadav
Copy link
Member Author

What's the design idea behind separating these two calls?

There are some additional checks that the user/client need to run on the bill data to ensure that claiming the service fees is actually worth it, given there will be some contract method call cost and balance transfer costs. The client will first fetch the current bill from the enclave, run those checks externally on them and will prepare a list of tx hashes that are worth claiming. They will then hit the 'export' endpoint with those tx hashes and get the final data and the signature required to claim those costs. The client will be then responsible for keeping the data and interacting with the billing contract to claim the bills.

src/billing_handler.rs Outdated Show resolved Hide resolved
let signature = hex::encode(rs.to_bytes().append(27 + v.to_byte()).as_slice());
let signed_data = hex::encode(signed_data.as_slice());

HttpResponse::Ok().json(json!({
Copy link
Member

Choose a reason for hiding this comment

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

Is it safe to assume that nonce is supposed to be unique? If yes, then can we save this data with given nonce as key. So that if in a case, client loses the bill, it can query bill again by providing the nonce. And maybe delete the cache after some timeout.

To avoid the case, where client got disconnected from enclave.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes nonce is supposed to be unique based on which the contract will catch duplication of bill data. We can store this data in cache for some time but I think once the client receives the data, they are supposed to claim the bill first thing and even if not that they are atleast supposed to store the bill data until they do. I don't know if we should populate the enclave with more data and affect their performance or capacity just to handle this. @roshanr95 Please comment.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, the contract will deduplicate receipts based on the nonce

Storing all, or even some, of them is probably overkill, can maybe just store the very last one that the client can then requery (/billing/latest). Assuming proper persistence (and API usage) is the responsibility of the client, the only real loss vector that remains is network issues while the response is being transmitted, just the last one should be fine to handle this.

Copy link
Member

Choose a reason for hiding this comment

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

We can also protect against duplicate requests by checking the nonce and seeing if it has changed since the last one 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Why is this required inside the enclave? Our major aim is to not allow duplicate requests to the billing contract because that's where the actual transactions happen. If client is sending a duplicate nonce to the enclave then it's their fault because then the enclave will think some balances have been settled when in fact the contract will reject it ,leading to lost bill info. If we were to check nonce inside the enclave then we'll have to check inside the list of all received yet and not just the last one which is probably an overkill.

Copy link
Member

Choose a reason for hiding this comment

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

Agree. Nonce check should also be enforced. Also storing the last bill is sufficient to handle the network issue.

src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await;
if signature.is_err() {
Copy link
Member

Choose a reason for hiding this comment

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

If signature is failing here, then it will fail again when billing/latest will be called. There is no change in parameters or other context data. Signature failing here seems fatal error to me. Double check where it can fail and how developers has suggested to handle those errors.

Copy link
Member Author

Choose a reason for hiding this comment

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

I looked into the error conditions and possibilities while signing prehash messages and saw if we're using a valid secret scalar key for signing (that I suppose we must be using) and a valid hashing algorithm that gives out a 32-byte hash (which is the case for our Keccak::v256() hasher) then it is very rare that an error would occur. Have added the error checks just in case something weird happens (because of outdated crate version or something).

Copy link
Member Author

Choose a reason for hiding this comment

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

Btw If we look more closely, there is an aggressive exit happening when serving the requests as well after failing to sign the requests/response data. So even after getting a successful response from the worker serving the app, we're throwing an error response to the user of the app. Is this an acceptable behaviour? @roshanr95 @vg-27

Copy link
Member

Choose a reason for hiding this comment

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

So, if signing is throwing exception then it will be there for every sign call. That means this application is not usable at all in that case, in production.

Then I think, if you just return exception here, that would be sufficient. Any exception due to implementation change, would be hit during testing phase of release.

You can remove the special handling here, it looks like you are trying to recover from error, but it's not. Kinda misleading.

Copy link
Member Author

@Ayush-Yadav Ayush-Yadav Feb 19, 2024

Choose a reason for hiding this comment

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

I was just handling it the way it was done when the serverless requests are getting processed by the enclave. I think that is more concerning since it is part of the code already in production. So shouldn't we add an exception here as well:-

pub async fn get_workerd_response(

port: u16,
req: HttpRequest,
body: actix_web::web::Bytes,
signer: &k256::ecdsa::SigningKey,
host_header: &str,

) -> Result<HttpResponse, anyhow::Error> {

let mut hasher = Keccak::v256();
hasher.update(b"|oyster-serverless-hasher|");

let timestamp = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)?
    .as_secs();
hasher.update(b"|timestamp|");
hasher.update(&timestamp.to_be_bytes());

hasher.update(b"|request|");
hasher.update(b"|method|");
hasher.update(req.method().to_string().as_bytes());
hasher.update(b"|pathandquery|");
hasher.update(
    req.uri()
        .path_and_query()
        .map(|x| x.as_str())
        .unwrap_or("")
        .as_bytes(),
);
hasher.update(b"|host|");
hasher.update(host_header.as_bytes());
hasher.update(b"|body|");
hasher.update(&body);

let port_str = port.to_string();
let req_url = "http://127.0.0.1:".to_string() + &port_str + "/";
let client = reqwest::Client::new();
let response = req
    .headers()
    .into_iter()
    .fold(
        client.request(req.method().clone(), req_url),
        |req, header| req.header(header.0.clone(), header.1.clone()),
    )
    .body(body)
    .send()
    .await?;
hasher.update(b"|response|");

let mut actix_resp = response.headers().into_iter().fold(
    HttpResponse::build(response.status()),
    |mut resp, header| {
        resp.append_header((header.0.clone(), header.1.clone()));
        resp
    },
);
let response_body = response.bytes().await?;

hasher.update(b"|body|");
hasher.update(&response_body);

let mut hash = [0u8; 32];
hasher.finalize(&mut hash);

let (rs, v) = signer.sign_prehash_recoverable(&hash)?;` 

Copy link
Member

Choose a reason for hiding this comment

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

Here in this case ? is used to pass on exception. No explicit handling required. Even for your case, I was suggesting just respond back with InternalError. And that would be fine.

Copy link
Member

@roshanr95 roshanr95 Feb 20, 2024

Choose a reason for hiding this comment

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

If signature is failing here, then it will fail again when billing/latest will be called. There is no change in parameters or other context data. Signature failing here seems fatal error to me. Double check where it can fail and how developers has suggested to handle those errors.

Depends on why it can fail and if it's consistent or not. For example, pretty sure it generates a random number inside, which can probably fail inconsistently.

Storing the last response is mainly just a safeguard I guess, if we don't even want to spend time really figuring out the failure conditions.

Copy link
Member

@roshanr95 roshanr95 Feb 20, 2024

Choose a reason for hiding this comment

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

Btw If we look more closely, there is an aggressive exit happening when serving the requests as well after failing to sign the requests/response data. So even after getting a successful response from the worker serving the app, we're throwing an error response to the user of the app. Is this an acceptable behaviour? @roshanr95 @vg-27

Yes, responses are insecure without a signature. No point in returning a response if not signed.

Copy link
Member

Choose a reason for hiding this comment

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

For example, pretty sure it generates a random number inside, which can probably fail inconsistently.

Yeah, that's right. Plain ECDSA signature uses random number. But here implementation seems to be using RFC6979, where k is computed deterministically.

Anyways, if there is possibility of inconsistent error. Let's keep the backup of billing data.

src/billing_handler.rs Show resolved Hide resolved
.sign_prehash_recoverable(&hash)
.context("Failed to sign billing data");
if sign.is_err() {
return HttpResponse::InternalServerError().body(format!("{}", sign.unwrap_err()));
Copy link
Member Author

Choose a reason for hiding this comment

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

@roshanr95 how to avoid aggressive exit here? by adding a retry mechanism or something?

let signature = hex::encode(rs.to_bytes().append(27 + v.to_byte()).as_slice());
let signed_data = hex::encode(signed_data.as_slice());

HttpResponse::Ok().json(json!({
Copy link
Member Author

Choose a reason for hiding this comment

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

Yes nonce is supposed to be unique based on which the contract will catch duplication of bill data. We can store this data in cache for some time but I think once the client receives the data, they are supposed to claim the bill first thing and even if not that they are atleast supposed to store the bill data until they do. I don't know if we should populate the enclave with more data and affect their performance or capacity just to handle this. @roshanr95 Please comment.

let signature = hex::encode(rs.to_bytes().append(27 + v.to_byte()).as_slice());
let signed_data = hex::encode(signed_data.as_slice());

HttpResponse::Ok().json(json!({
Copy link
Member Author

Choose a reason for hiding this comment

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

Why is this required inside the enclave? Our major aim is to not allow duplicate requests to the billing contract because that's where the actual transactions happen. If client is sending a duplicate nonce to the enclave then it's their fault because then the enclave will think some balances have been settled when in fact the contract will reject it ,leading to lost bill info. If we were to check nonce inside the enclave then we'll have to check inside the list of all received yet and not just the last one which is probably an overkill.

src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await;
if signature.is_err() {
Copy link
Member Author

Choose a reason for hiding this comment

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

I looked into the error conditions and possibilities while signing prehash messages and saw if we're using a valid secret scalar key for signing (that I suppose we must be using) and a valid hashing algorithm that gives out a 32-byte hash (which is the case for our Keccak::v256() hasher) then it is very rare that an error would occur. Have added the error checks just in case something weird happens (because of outdated crate version or something).

src/billing_handler.rs Show resolved Hide resolved
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await;
if signature.is_err() {
Copy link
Member Author

Choose a reason for hiding this comment

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

Btw If we look more closely, there is an aggressive exit happening when serving the requests as well after failing to sign the requests/response data. So even after getting a successful response from the worker serving the app, we're throwing an error response to the user of the app. Is this an acceptable behaviour? @roshanr95 @vg-27

Copy link
Member

@vg-27 vg-27 left a comment

Choose a reason for hiding this comment

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

3 comments are still unresolved.

src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Show resolved Hide resolved
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await;
if signature.is_err() {
Copy link
Member

Choose a reason for hiding this comment

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

So, if signing is throwing exception then it will be there for every sign call. That means this application is not usable at all in that case, in production.

Then I think, if you just return exception here, that would be sufficient. Any exception due to implementation change, would be hit during testing phase of release.

You can remove the special handling here, it looks like you are trying to recover from error, but it's not. Kinda misleading.

Copy link
Member

@vg-27 vg-27 left a comment

Choose a reason for hiding this comment

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

LGTM. Let's get approval from Roshan too.

src/billing_handler.rs Outdated Show resolved Hide resolved
src/billing_handler.rs Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants