1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
pub mod client;
pub mod participant;
use crate::{
api::{ApiClient, HttpApiClient, HttpApiClientError},
mobile_client::{
client::{ClientStateMachine, LocalModel},
participant::ParticipantSettings,
},
};
use thiserror::Error;
use xaynet_core::{
crypto::{SecretSigningKey, SigningKeyPair},
mask::Model,
InitError,
};
#[derive(Debug, Error)]
/// Mobile client errors
pub enum MobileClientError {
#[error("failed to deserialize mobile client: {0}")]
/// Failed to deserialize mobile client.
Deserialize(#[from] bincode::Error),
#[error("failed to initialize crypto module: {0}")]
/// Failed to initialize crypto module.
Init(#[from] InitError),
#[error("failed to initialize runtime: {0}")]
/// Failed to initialize runtime.
Runtime(#[from] std::io::Error),
#[error("API request failed: {0}")]
/// API request failed.
Api(#[from] HttpApiClientError),
}
pub struct MobileClient {
api: HttpApiClient,
local_model: LocalModelCache,
client_state: ClientStateMachine,
}
impl MobileClient {
/// Initializes a fresh client. This method only needs to be called once.
///
/// To serialize and restore a client use the [`MobileClient::serialize`] and
/// [`MobileClient::restore`]
///
/// # Errors
///
/// Fails if the crypto module cannot be initialized.
pub fn init(
url: &str,
participant_settings: ParticipantSettings,
) -> Result<Self, MobileClientError> {
// It is critical that the initialization of sodiumoxide is successful.
// We'd better not run the client than having a broken crypto.
//
// Refs:
// https://doc.libsodium.org/usage
// https://github.com/jedisct1/libsodium/issues/908
let client_state = ClientStateMachine::new(participant_settings)?;
Ok(Self::new(url, client_state))
}
/// Restores a client from its serialized state.
///
/// # Errors
///
/// Fails if the serialized state is corrupted and the client cannot be restored
/// or if the crypto module cannot be initialized.
pub fn restore(url: &str, bytes: &[u8]) -> Result<Self, MobileClientError> {
let client_state: ClientStateMachine = bincode::deserialize(bytes)?;
Ok(Self::new(url, client_state))
}
fn new(url: &str, client_state: ClientStateMachine) -> Self {
let api = HttpApiClient::new(url);
Self {
api,
client_state,
local_model: LocalModelCache(None),
}
}
/// Serializes the current state of the client.
///
/// # Note
///
/// The serialized state is **not encrypted** and contains sensitive data such as the
/// participant's private key. Therefore, the user of the [`MobileClient`] **must** ensure
/// that the serialized state is stored in a safe place.
pub fn serialize(&self) -> Vec<u8> {
// Safe to unwrap:
//
// - all sequences have known length
// - an iterator is an example for a sequence with an unknown length
// - no untagged enum
//
// Refs:
// - https://github.com/servo/bincode/issues/293
// - https://github.com/servo/bincode/issues/255
// - https://github.com/servo/bincode/issues/130#issuecomment-284641263
bincode::serialize(&self.client_state).unwrap()
}
/// Fetches and returns the latest global model from the coordinator.
/// Returns `None` if no global model is available.
///
/// # Errors
///
/// Fails if the runtime cannot be initialized or if an API request has failed.
pub fn get_global_model(&mut self) -> Result<Option<Model>, MobileClientError> {
Self::runtime()?
.block_on(async { self.api.get_model().await })
.map_err(|err| err.into())
}
/// Tries to proceed with the current client task.
/// This will consume the current state of the client and produces a new one.
///
/// # Errors
///
/// Fails if the runtime cannot be initialized.
/// In this case the state of the client remains unchanged and is returned
/// along with the error.
pub fn try_to_proceed(self) -> Result<Self, (Self, MobileClientError)> {
let mut runtime = match Self::runtime() {
Ok(runtime) => runtime,
// We don't want to loose the current client because of a runtime error.
// Therefore we return the error as well as the current client.
Err(err) => return Err((self, err.into())),
};
let MobileClient {
mut api,
mut local_model,
client_state,
} = self;
let client_state =
runtime.block_on(async { client_state.next(&mut api, &mut local_model).await });
Ok(Self {
api,
local_model,
client_state,
})
}
/// Returns the current state of the client.
pub fn get_current_state(&self) -> ClientStateName {
match self.client_state {
ClientStateMachine::Awaiting(_) => ClientStateName::Awaiting,
ClientStateMachine::Sum(_) => ClientStateName::Sum,
ClientStateMachine::Update(_) => ClientStateName::Update,
ClientStateMachine::Sum2(_) => ClientStateName::Sum2,
}
}
/// Sets the local model.
///
/// The local model is only sent if the client has been selected as an update client.
/// If the client is an update client and no local model is available, the client remains
/// in this state until a local model has been set or a new round has been started by the
/// coordinator.
pub fn set_local_model(&mut self, model: Model) {
self.local_model.set_local_model(model);
}
/// Creates a new participant secret key.
///
/// The secret key is part of the [`ParticipantSettings`] which are required for the first
/// initialization of the client.
pub fn create_participant_secret_key() -> SecretSigningKey {
let SigningKeyPair { secret, .. } = SigningKeyPair::generate();
secret
}
fn runtime() -> Result<tokio::runtime::Runtime, std::io::Error> {
// Following the code of tokio, the creation of the I/O driver can result in an error.
// It is not documented what exact condition can cause an error. Therefore we don't unwrap
// here.
tokio::runtime::Builder::new()
.basic_scheduler()
.enable_all()
.build()
}
}
#[derive(Debug)]
pub enum ClientStateName {
Awaiting,
Sum,
Update,
Sum2,
}
struct LocalModelCache(Option<Model>);
impl LocalModelCache {
fn set_local_model(&mut self, model: Model) {
self.0 = Some(model);
}
}
#[async_trait]
impl LocalModel for LocalModelCache {
async fn get_local_model(&mut self) -> Option<Model> {
self.0.clone()
}
}