Add proof of level 3 or 4, bucket. (Still needs to be debugged.)
This commit is contained in:
parent
a6c5d789e7
commit
9cd9e3fab7
|
@ -4,8 +4,8 @@ reports (indicating that a bridge has been accessed).
|
||||||
|
|
||||||
The user presents their current Lox credential:
|
The user presents their current Lox credential:
|
||||||
- id: blinded
|
- id: blinded
|
||||||
- bucket: blinded (but proven to be beta in provided beta*H)
|
- bucket: blinded (but proven to be bucket in provided bucket*H)
|
||||||
- trust_level: revealed to be 3 or higher
|
- trust_level: blinded (but proven to be 3 or higher)
|
||||||
- level_since: blinded
|
- level_since: blinded
|
||||||
- invites_remaining: blinded
|
- invites_remaining: blinded
|
||||||
- blockages: blinded
|
- blockages: blinded
|
||||||
|
@ -13,79 +13,99 @@ The user presents their current Lox credential:
|
||||||
This protocol does not expose the credential's ID and does not issue a
|
This protocol does not expose the credential's ID and does not issue a
|
||||||
new Lox credential. The user does not receive a response.
|
new Lox credential. The user does not receive a response.
|
||||||
|
|
||||||
Right now, this doesn't work. Also, it doesn't yet check the user's bucket,
|
Due to the way the level proof is implemented, this protocol has
|
||||||
so it's just "Prove that the user's level is at least 3."
|
hardcoded assumptions that the level is 3-4.
|
||||||
|
|
||||||
|
Right now, this doesn't work.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use curve25519_dalek::ristretto::{RistrettoBasepointTable, RistrettoPoint};
|
use curve25519_dalek::ristretto::{RistrettoBasepointTable, RistrettoPoint};
|
||||||
use curve25519_dalek::scalar::Scalar;
|
use curve25519_dalek::scalar::Scalar;
|
||||||
use curve25519_dalek::traits::IsIdentity;
|
use curve25519_dalek::traits::IsIdentity;
|
||||||
|
|
||||||
use lox_zkp::{CompactProof, ProofError, Transcript};
|
use lox_zkp::{CompactProof, ProofError, Transcript};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::Sha512;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use super::super::cred;
|
use super::super::cred;
|
||||||
use super::super::scalar_u32;
|
use super::super::scalar_u32;
|
||||||
use super::super::{BridgeAuth, IssuerPubKey};
|
use super::super::{BridgeAuth, IssuerPubKey};
|
||||||
use super::super::{CMZ_A, CMZ_A_TABLE};
|
use super::super::{CMZ_A, CMZ_A_TABLE};
|
||||||
|
|
||||||
pub const MIN_TRUST_LEVEL: u32 = 3;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
// Fields for blind showing the Lox credential
|
// Fields for blind showing the Lox credential
|
||||||
P: RistrettoPoint,
|
P: RistrettoPoint,
|
||||||
CId: RistrettoPoint,
|
CId: RistrettoPoint,
|
||||||
CBucket: RistrettoPoint,
|
CBucket: RistrettoPoint,
|
||||||
level: Scalar,
|
CLevel: RistrettoPoint,
|
||||||
CSince: RistrettoPoint,
|
CSince: RistrettoPoint,
|
||||||
CInvRemain: RistrettoPoint,
|
CInvRemain: RistrettoPoint,
|
||||||
CBlockages: RistrettoPoint,
|
CBlockages: RistrettoPoint,
|
||||||
CQ: RistrettoPoint,
|
CQ: RistrettoPoint,
|
||||||
|
|
||||||
|
// Fields for proving which bucket we have
|
||||||
|
H: RistrettoPoint,
|
||||||
|
BP: RistrettoPoint,
|
||||||
|
|
||||||
|
// Fields for proving 3 <= trust_level <= 4
|
||||||
|
// CG can be computed by verifier
|
||||||
|
CGsq: RistrettoPoint,
|
||||||
|
|
||||||
// The combined lox_zkp
|
// The combined lox_zkp
|
||||||
piUser: CompactProof,
|
piUser: CompactProof,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct State {
|
|
||||||
level: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct Response {}
|
|
||||||
|
|
||||||
define_proof! {
|
define_proof! {
|
||||||
requestproof,
|
requestproof,
|
||||||
"Positive Report Request",
|
"Positive Report Request",
|
||||||
(id, bucket, since, invremain, blockages, zid, zbucket, zsince, zinvremain, zblockages, negzQ),
|
(id, bucket, level, since, invremain, blockages,
|
||||||
(P, CId, CBucket, CSince, CInvRemain, CBlockages, V, Xid, Xbucket, Xsince, Xinvremain, Xblockages),//, H, HBucket),
|
zid, zbucket, zlevel, zsince, zinvremain, zblockages, negzQ,
|
||||||
|
g, zg, wg, yg),
|
||||||
|
(P, CId, CBucket, CLevel, CSince, CInvRemain, CBlockages,
|
||||||
|
V, Xid, Xbucket, Xlevel, Xsince, Xinvremain, Xblockages,
|
||||||
|
H, BP, CG, CGsq),
|
||||||
(A):
|
(A):
|
||||||
// Blind showing of the Lox credential
|
// Blind showing of the Lox credential
|
||||||
CId = (id*P + zid*A),
|
CId = (id*P + zid*A),
|
||||||
CBucket = (bucket*P + zbucket*A),
|
CBucket = (bucket*P + zbucket*A),
|
||||||
|
CLevel = (level*P + zlevel*A),
|
||||||
CSince = (since*P + zsince*A),
|
CSince = (since*P + zsince*A),
|
||||||
CInvRemain = (invremain*P + zinvremain*A),
|
CInvRemain = (invremain*P + zinvremain*A),
|
||||||
CBlockages = (blockages*P + zblockages*A),
|
CBlockages = (blockages*P + zblockages*A),
|
||||||
V = (zid*Xid + zbucket*Xbucket + zsince*Xsince + zinvremain*Xinvremain + zblockages*Xblockages + negzQ*A)
|
V = (zid*Xid + zbucket*Xbucket + zlevel*Xlevel + zsince*Xsince + zinvremain*Xinvremain + zblockages*Xblockages + negzQ*A),
|
||||||
|
// Prove bucket is same bucket used in BP
|
||||||
|
BP = (bucket*H),
|
||||||
|
// Prove CLevel encodes a value of 3 or 4
|
||||||
|
// First prove g is a bit by proving that g = g^2
|
||||||
|
CG = (g*P + zg*A), CGsq = (g*CG + wg*A), CGsq = (g*P + yg*A)
|
||||||
|
// The verifier will compute CG = CLevel - 3P
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(
|
pub fn request(
|
||||||
lox_cred: &cred::Lox,
|
lox_cred: &cred::Lox,
|
||||||
lox_pub: &IssuerPubKey,
|
lox_pub: &IssuerPubKey,
|
||||||
) -> Result<(Request, State), ProofError> {
|
) -> Result<Request, ProofError> {
|
||||||
let A: &RistrettoPoint = &CMZ_A;
|
let A: &RistrettoPoint = &CMZ_A;
|
||||||
let Atable: &RistrettoBasepointTable = &CMZ_A_TABLE;
|
let Atable: &RistrettoBasepointTable = &CMZ_A_TABLE;
|
||||||
|
|
||||||
|
// TODO: Where should this go? For efficiency, this should probably be global
|
||||||
|
let today: u32 = time::OffsetDateTime::now_utc().date()
|
||||||
|
.to_julian_day()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
let H: RistrettoPoint = RistrettoPoint::hash_from_bytes::<Sha512>(format!("PR Generator H for {}",today).as_bytes());
|
||||||
|
let Htable: RistrettoBasepointTable = RistrettoBasepointTable::create(&H);
|
||||||
|
|
||||||
// Ensure that the credential can be correctly shown: it must be the case
|
// Ensure that the credential can be correctly shown: it must be the case
|
||||||
// that trust_level >= MIN_TRUST_LEVEL
|
// that trust_level is 3 or 4
|
||||||
let level: u32 = match scalar_u32(&lox_cred.trust_level) {
|
let level: u32 = match scalar_u32(&lox_cred.trust_level) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Err(ProofError::VerificationFailure),
|
None => return Err(ProofError::VerificationFailure),
|
||||||
};
|
};
|
||||||
if level < MIN_TRUST_LEVEL {
|
// level must be 3 + one bit or we need to add more bits
|
||||||
|
if level < 3 || level > 4 {
|
||||||
return Err(ProofError::VerificationFailure);
|
return Err(ProofError::VerificationFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,11 +120,13 @@ pub fn request(
|
||||||
// Form Pedersen commitments to the blinded attributes
|
// Form Pedersen commitments to the blinded attributes
|
||||||
let zid = Scalar::random(&mut rng);
|
let zid = Scalar::random(&mut rng);
|
||||||
let zbucket = Scalar::random(&mut rng);
|
let zbucket = Scalar::random(&mut rng);
|
||||||
|
let zlevel = Scalar::random(&mut rng);
|
||||||
let zsince = Scalar::random(&mut rng);
|
let zsince = Scalar::random(&mut rng);
|
||||||
let zinvremain = Scalar::random(&mut rng);
|
let zinvremain = Scalar::random(&mut rng);
|
||||||
let zblockages = Scalar::random(&mut rng);
|
let zblockages = Scalar::random(&mut rng);
|
||||||
let CId = lox_cred.id * P + &zid * Atable;
|
let CId = lox_cred.id * P + &zid * Atable;
|
||||||
let CBucket = lox_cred.bucket * P + &zbucket * Atable;
|
let CBucket = lox_cred.bucket * P + &zbucket * Atable;
|
||||||
|
let CLevel = lox_cred.trust_level * P + &zlevel * Atable;
|
||||||
let CSince = lox_cred.level_since * P + &zsince * Atable;
|
let CSince = lox_cred.level_since * P + &zsince * Atable;
|
||||||
let CInvRemain = lox_cred.invites_remaining * P + &zinvremain * Atable;
|
let CInvRemain = lox_cred.invites_remaining * P + &zinvremain * Atable;
|
||||||
let CBlockages = lox_cred.blockages * P + &zblockages * Atable;
|
let CBlockages = lox_cred.blockages * P + &zblockages * Atable;
|
||||||
|
@ -119,11 +141,36 @@ pub fn request(
|
||||||
// Compute the "error factor"
|
// Compute the "error factor"
|
||||||
let V = zid * lox_pub.X[1]
|
let V = zid * lox_pub.X[1]
|
||||||
+ zbucket * lox_pub.X[2]
|
+ zbucket * lox_pub.X[2]
|
||||||
|
+ zlevel * lox_pub.X[3]
|
||||||
+ zsince * lox_pub.X[4]
|
+ zsince * lox_pub.X[4]
|
||||||
+ zinvremain * lox_pub.X[5]
|
+ zinvremain * lox_pub.X[5]
|
||||||
+ zblockages * lox_pub.X[6]
|
+ zblockages * lox_pub.X[6]
|
||||||
+ &negzQ * Atable;
|
+ &negzQ * Atable;
|
||||||
|
|
||||||
|
// Compute BP for proving knowledge of bucket
|
||||||
|
let BP = &lox_cred.bucket * &Htable;
|
||||||
|
|
||||||
|
// Proof that 3 <= trust_level
|
||||||
|
|
||||||
|
let g: Scalar = (level - 3).into();
|
||||||
|
|
||||||
|
// Pick random factor for the Pedersen commitment
|
||||||
|
let wg = Scalar::random(&mut rng);
|
||||||
|
|
||||||
|
// zg equals zlevel so that
|
||||||
|
// 3*P + CG
|
||||||
|
// = 3*P + (g*P + zg*A)
|
||||||
|
// = (3+g)*P + zlevel*A
|
||||||
|
// = level*P + zlevel*A
|
||||||
|
// = CLevel
|
||||||
|
let zg = zlevel;
|
||||||
|
|
||||||
|
let yg = wg + g * zg;
|
||||||
|
|
||||||
|
let CG = g * P + &zg * Atable;
|
||||||
|
|
||||||
|
let CGsq = g * P + &yg * Atable;
|
||||||
|
|
||||||
// Construct the proof
|
// Construct the proof
|
||||||
let mut transcript = Transcript::new(b"proof of level 3 cred");
|
let mut transcript = Transcript::new(b"proof of level 3 cred");
|
||||||
let piUser = requestproof::prove_compact(
|
let piUser = requestproof::prove_compact(
|
||||||
|
@ -133,73 +180,84 @@ pub fn request(
|
||||||
P: &P,
|
P: &P,
|
||||||
CId: &CId,
|
CId: &CId,
|
||||||
CBucket: &CBucket,
|
CBucket: &CBucket,
|
||||||
|
CLevel: &CLevel,
|
||||||
CSince: &CSince,
|
CSince: &CSince,
|
||||||
CInvRemain: &CInvRemain,
|
CInvRemain: &CInvRemain,
|
||||||
CBlockages: &CBlockages,
|
CBlockages: &CBlockages,
|
||||||
V: &V,
|
V: &V,
|
||||||
Xid: &lox_pub.X[1],
|
Xid: &lox_pub.X[1],
|
||||||
Xbucket: &lox_pub.X[2],
|
Xbucket: &lox_pub.X[2],
|
||||||
|
Xlevel: &lox_pub.X[3],
|
||||||
Xsince: &lox_pub.X[4],
|
Xsince: &lox_pub.X[4],
|
||||||
Xinvremain: &lox_pub.X[5],
|
Xinvremain: &lox_pub.X[5],
|
||||||
Xblockages: &lox_pub.X[6],
|
Xblockages: &lox_pub.X[6],
|
||||||
|
H: &H,
|
||||||
|
BP: &BP,
|
||||||
|
CG: &CG,
|
||||||
|
CGsq: &CGsq,
|
||||||
id: &lox_cred.id,
|
id: &lox_cred.id,
|
||||||
bucket: &lox_cred.bucket,
|
bucket: &lox_cred.bucket,
|
||||||
|
level: &lox_cred.trust_level,
|
||||||
since: &lox_cred.level_since,
|
since: &lox_cred.level_since,
|
||||||
invremain: &lox_cred.invites_remaining,
|
invremain: &lox_cred.invites_remaining,
|
||||||
blockages: &lox_cred.blockages,
|
blockages: &lox_cred.blockages,
|
||||||
zid: &zid,
|
zid: &zid,
|
||||||
zbucket: &zbucket,
|
zbucket: &zbucket,
|
||||||
|
zlevel: &zlevel,
|
||||||
zsince: &zsince,
|
zsince: &zsince,
|
||||||
zinvremain: &zinvremain,
|
zinvremain: &zinvremain,
|
||||||
zblockages: &zblockages,
|
zblockages: &zblockages,
|
||||||
negzQ: &negzQ,
|
negzQ: &negzQ,
|
||||||
|
g: &g,
|
||||||
|
zg: &zg,
|
||||||
|
wg: &wg,
|
||||||
|
yg: &yg,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
Ok((
|
Ok(
|
||||||
Request {
|
Request {
|
||||||
P,
|
P,
|
||||||
CId,
|
CId,
|
||||||
CBucket,
|
CBucket,
|
||||||
level: lox_cred.trust_level,
|
CLevel,
|
||||||
CSince,
|
CSince,
|
||||||
CInvRemain,
|
CInvRemain,
|
||||||
CBlockages,
|
CBlockages,
|
||||||
CQ,
|
CQ,
|
||||||
|
H,
|
||||||
|
BP,
|
||||||
|
CGsq,
|
||||||
piUser,
|
piUser,
|
||||||
},
|
}
|
||||||
State {
|
)
|
||||||
level: lox_cred.trust_level,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BridgeAuth {
|
impl BridgeAuth {
|
||||||
/// Receive a positive report request
|
/// Receive a positive report request
|
||||||
pub fn handle_positive_report(&mut self, req: Request) -> Result<Response, ProofError> {
|
pub fn handle_positive_report(&mut self, req: Request) -> Result<(), ProofError> {
|
||||||
let A: &RistrettoPoint = &CMZ_A;
|
let A: &RistrettoPoint = &CMZ_A;
|
||||||
|
|
||||||
let level: u32 = match scalar_u32(&req.level) {
|
if req.P.is_identity() {
|
||||||
Some(v) => v,
|
|
||||||
None => return Err(ProofError::VerificationFailure),
|
|
||||||
};
|
|
||||||
|
|
||||||
if req.P.is_identity() || level < MIN_TRUST_LEVEL {
|
|
||||||
return Err(ProofError::VerificationFailure);
|
return Err(ProofError::VerificationFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recompute the "error factor" using knowledge of our own
|
// Recompute the "error factor" using knowledge of our own
|
||||||
// (the issuer's) private key instead of knowledge of the
|
// (the issuer's) private key instead of knowledge of the
|
||||||
// hidden attributes
|
// hidden attributes
|
||||||
let Vprime = (self.lox_priv.x[0] + self.lox_priv.x[3] * req.level) * req.P
|
let Vprime = self.lox_priv.x[0] * req.P
|
||||||
+ self.lox_priv.x[1] * req.CId
|
+ self.lox_priv.x[1] * req.CId
|
||||||
+ self.lox_priv.x[2] * req.CBucket
|
+ self.lox_priv.x[2] * req.CBucket
|
||||||
|
+ self.lox_priv.x[3] * req.CLevel
|
||||||
+ self.lox_priv.x[4] * req.CSince
|
+ self.lox_priv.x[4] * req.CSince
|
||||||
+ self.lox_priv.x[5] * req.CInvRemain
|
+ self.lox_priv.x[5] * req.CInvRemain
|
||||||
+ self.lox_priv.x[6] * req.CBlockages
|
+ self.lox_priv.x[6] * req.CBlockages
|
||||||
- req.CQ;
|
- req.CQ;
|
||||||
|
|
||||||
|
// Recompute CG
|
||||||
|
let CG = req.CLevel - Scalar::from(3 as u8) * req.P;
|
||||||
|
|
||||||
// TODO: Failure happens in verify_compact
|
// TODO: Failure happens in verify_compact
|
||||||
// Verify the zkp
|
// Verify the zkp
|
||||||
let mut transcript = Transcript::new(b"positive report request");
|
let mut transcript = Transcript::new(b"positive report request");
|
||||||
|
@ -211,23 +269,24 @@ impl BridgeAuth {
|
||||||
P: &req.P.compress(),
|
P: &req.P.compress(),
|
||||||
CId: &req.CId.compress(),
|
CId: &req.CId.compress(),
|
||||||
CBucket: &req.CBucket.compress(),
|
CBucket: &req.CBucket.compress(),
|
||||||
|
CLevel: &req.CLevel.compress(),
|
||||||
CSince: &req.CSince.compress(),
|
CSince: &req.CSince.compress(),
|
||||||
CInvRemain: &req.CInvRemain.compress(),
|
CInvRemain: &req.CInvRemain.compress(),
|
||||||
CBlockages: &req.CBlockages.compress(),
|
CBlockages: &req.CBlockages.compress(),
|
||||||
V: &Vprime.compress(),
|
V: &Vprime.compress(),
|
||||||
Xid: &self.lox_pub.X[1].compress(),
|
Xid: &self.lox_pub.X[1].compress(),
|
||||||
Xbucket: &self.lox_pub.X[2].compress(),
|
Xbucket: &self.lox_pub.X[2].compress(),
|
||||||
|
Xlevel: &self.lox_pub.X[3].compress(),
|
||||||
Xsince: &self.lox_pub.X[4].compress(),
|
Xsince: &self.lox_pub.X[4].compress(),
|
||||||
Xinvremain: &self.lox_pub.X[5].compress(),
|
Xinvremain: &self.lox_pub.X[5].compress(),
|
||||||
Xblockages: &self.lox_pub.X[6].compress(),
|
Xblockages: &self.lox_pub.X[6].compress(),
|
||||||
|
H: &req.H.compress(),
|
||||||
|
BP: &req.BP.compress(),
|
||||||
|
CG: &CG.compress(),
|
||||||
|
CGsq: &req.CGsq.compress(),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(Response {})
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle the response to the request, returning true if the proof is valid.
|
|
||||||
pub fn handle_response(_state: State, _resp: Response) -> Result<(), ProofError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -374,21 +374,18 @@ impl TestHarness {
|
||||||
|
|
||||||
fn positive_report(&mut self, cred: &cred::Lox) -> PerfStat {
|
fn positive_report(&mut self, cred: &cred::Lox) -> PerfStat {
|
||||||
let req_start = Instant::now();
|
let req_start = Instant::now();
|
||||||
let (req, state) = positive_report::request(cred, &self.ba.lox_pub).unwrap();
|
let req = positive_report::request(cred, &self.ba.lox_pub).unwrap();
|
||||||
let encoded: Vec<u8> = bincode::serialize(&req).unwrap();
|
let encoded: Vec<u8> = bincode::serialize(&req).unwrap();
|
||||||
let req_t = req_start.elapsed();
|
let req_t = req_start.elapsed();
|
||||||
let req_len = encoded.len();
|
let req_len = encoded.len();
|
||||||
|
|
||||||
let resp_start = Instant::now();
|
let resp_start = Instant::now();
|
||||||
let decoded = bincode::deserialize(&encoded[..]).unwrap();
|
let decoded = bincode::deserialize(&encoded[..]).unwrap();
|
||||||
let resp = self.ba.handle_positive_report(decoded).unwrap();
|
self.ba.handle_positive_report(decoded).unwrap();
|
||||||
let encoded_resp: Vec<u8> = bincode::serialize(&resp).unwrap();
|
|
||||||
let resp_t = resp_start.elapsed();
|
let resp_t = resp_start.elapsed();
|
||||||
let resp_len = encoded_resp.len();
|
let resp_len = 0;
|
||||||
|
|
||||||
let resp_handle_start = Instant::now();
|
let resp_handle_start = Instant::now();
|
||||||
let decode_resp = bincode::deserialize(&encoded_resp[..]).unwrap();
|
|
||||||
positive_report::handle_response(state, decode_resp).unwrap();
|
|
||||||
let resp_handle_t = resp_handle_start.elapsed();
|
let resp_handle_t = resp_handle_start.elapsed();
|
||||||
|
|
||||||
PerfStat {
|
PerfStat {
|
||||||
|
@ -1405,6 +1402,17 @@ fn test_positive_report() {
|
||||||
|
|
||||||
// Submit positive report
|
// Submit positive report
|
||||||
let _pr_perf_stat = th.positive_report(&cred3);
|
let _pr_perf_stat = th.positive_report(&cred3);
|
||||||
|
|
||||||
|
// Time passes
|
||||||
|
th.advance_days(60);
|
||||||
|
|
||||||
|
// Go up to level 4
|
||||||
|
let (_four_perf_stat, cred4) = th.level_up(&cred3);
|
||||||
|
assert!(scalar_u32(&cred4.trust_level).unwrap() == 4);
|
||||||
|
assert!(th.ba.verify_lox(&cred4));
|
||||||
|
|
||||||
|
// Submit positive report
|
||||||
|
let _pr_perf_stat2 = th.positive_report(&cred4);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_test_results(perf_stat: PerfStat) {
|
fn print_test_results(perf_stat: PerfStat) {
|
||||||
|
|
Loading…
Reference in New Issue