This repository has been archived by the owner on Jun 21, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
lib.rs
3329 lines (2964 loc) · 135 KB
/
lib.rs
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (C) 2021-2022 Prosopo (UK) Ltd.
// This file is part of provider <https://github.com/prosopo/provider>.
//
// provider is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// provider is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with provider. If not, see <http://www.gnu.org/licenses/>.
#![cfg_attr(not(feature = "std"), no_std)]
pub use self::prosopo::{Prosopo, ProsopoRef};
/// Print and return an error in ink
macro_rules! err {
($err:expr) => {{
Err(get_self!().print_err($err, function_name!()))
}};
}
// ($err:expr) => (
// |$err| crate::print_error($err, function_name!(), get_self!().env().block_number(), get_self!().env().caller())
// );
macro_rules! err_fn {
($err:expr) => {
|| get_self!().print_err($err, function_name!())
};
}
macro_rules! lazy_push {
($lazy:expr, $value:expr) => {
let mut vec = $lazy.get_or_default();
vec.push($value);
$lazy.set(&vec);
};
}
/// Concatenate two arrays (a and b) into a new array (c)
fn concat_u8<const A: usize, const B: usize, const C: usize>(a: &[u8; A], b: &[u8; B]) -> [u8; C] {
let mut c = [0; C];
c[..A].copy_from_slice(a);
c[A..A + B].copy_from_slice(b);
c
}
#[allow(unused_macros)]
#[named_functions_macro::named_functions] // allows the use of the function_name!() macro
#[inject_self_macro::inject_self] // allows the use of the get_self!() macro
#[ink::contract]
pub mod prosopo {
use ink::env::debug_println as debug;
use ink::env::hash::{Blake2x128, Blake2x256, CryptoHash, HashOutput};
use ink::prelude::collections::btree_set::BTreeSet;
use ink::prelude::vec::Vec;
use ink::storage::Lazy;
#[allow(unused_imports)] // do not remove StorageLayout, it is used in derives
use ink::storage::{traits::StorageLayout, Mapping};
/// GovernanceStatus relates to DApps and Providers and determines if they are active or not
#[derive(Default, PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum GovernanceStatus {
Active, // active and available for use
Suspended, // a state that should be used for dapps/providers whose stake drops below the minimum required or who are being investigated as part of a slashing event etc.
#[default]
Deactivated, // temporarily inactive
}
/// CaptchaStatus is the status of a CaptchaSolutionCommitment, submitted by a DappUser
#[derive(Default, PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum CaptchaStatus {
Pending,
Approved,
#[default]
Disapproved,
}
/// Payee is the recipient of any fees that are paid when a CaptchaSolutionCommitment is approved
#[derive(Default, PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum Payee {
Provider,
#[default]
Dapp,
}
/// Dapps must be able to filter Providers by their Payee when they are searching for a Provider
#[derive(Default, PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum DappPayee {
Provider,
Dapp,
#[default]
Any,
}
impl TryFrom<DappPayee> for Payee {
type Error = ();
fn try_from(dapp_payee: DappPayee) -> Result<Self, Self::Error> {
match dapp_payee {
DappPayee::Provider => Ok(Payee::Provider),
DappPayee::Dapp => Ok(Payee::Dapp),
DappPayee::Any => Err(()),
}
}
}
impl TryFrom<Payee> for DappPayee {
type Error = ();
fn try_from(payee: Payee) -> Result<Self, Self::Error> {
match payee {
Payee::Provider => Ok(DappPayee::Provider),
Payee::Dapp => Ok(DappPayee::Dapp),
}
}
}
/// Providers are suppliers of human verification methods (captchas, etc.) to DappUsers, either
/// paying or receiving a fee for this service.
#[derive(PartialEq, Debug, Eq, Clone, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct Provider {
status: GovernanceStatus,
balance: Balance,
// an amount in the base unit of the default parachain token (e.g. Planck on chains using DOT)
fee: u32,
payee: Payee,
service_origin: Vec<u8>,
dataset_id: Hash,
}
/// RandomProvider is selected randomly by the contract for the client side application
#[derive(PartialEq, Debug, Eq, Clone, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct RandomProvider {
provider_id: AccountId,
provider: Provider,
block_number: u32,
dataset_id_content: Hash,
}
/// Operators are controllers of this contract with admin rights
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct Operator {
status: GovernanceStatus,
}
/// Enum for various types of captcha
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum CaptchaType {
ImageGrid,
}
/// CaptchaData contains the hashed root of a Provider's dataset and is used to verify that
/// the captchas received by a DappUser did belong to the Provider's original dataset
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct CaptchaData {
provider: AccountId,
dataset_id: Hash,
dataset_id_content: Hash,
captcha_type: CaptchaType,
}
/// CaptchaSolutionCommitments are submitted by DAppUsers upon completion of one or more
/// Captchas. They serve as proof of captcha completion to the outside world and can be used
/// in dispute resolution.
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct CaptchaSolutionCommitment {
// the Dapp User Account
account: AccountId,
// The captcha dataset id (dataset_id in Provider / CaptchaData)
dataset_id: Hash,
// Status of this solution - correct / incorrect?
status: CaptchaStatus,
// The Dapp Contract AccountId that the Dapp User wants to interact with
contract: AccountId,
// The Provider AccountId that is permitted to approve or disapprove the commitment
provider: AccountId,
// Time of completion
completed_at: Timestamp,
}
/// DApps are distributed apps who want their users to be verified by Providers, either paying
/// or receiving a fee for this service.
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct Dapp {
status: GovernanceStatus,
balance: Balance,
owner: AccountId,
min_difficulty: u16,
payee: DappPayee,
}
/// Users are the users of DApps that are required to be verified as human before they are
/// allowed to interact with the DApps' contracts.
#[derive(PartialEq, Debug, Eq, Clone, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct User {
// the last n commitment hashes in chronological order (most recent first)
history: Vec<Hash>, // lookup the commitment in Prosopo.commitments
}
/// The summary of a user's captcha history using the n most recent captcha results limited by age and number of captcha results
#[derive(PartialEq, Debug, Eq, Clone, scale::Encode, scale::Decode, Copy)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct UserHistorySummary {
pub correct: u16,
pub incorrect: u16,
pub score: u8,
}
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct LastCorrectCaptcha {
pub before_ms: u64,
pub dapp_id: AccountId,
}
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct OperatorVote {
pub account_id: AccountId,
pub vote: Option<Vote>,
}
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub struct ProviderState {
pub status: GovernanceStatus,
pub payee: Payee,
}
/// Set of actions which can be performed by operators given a successful vote
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum Vote {
SetCodeHash([u8; 32]), // accepts the code hash
Withdraw(AccountId, Balance), // accepts the recipient and the amount
Terminate(AccountId), // accepts the account to send the remaining balance of this contract to after termination
}
/// The status of the current voting process.
#[derive(PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum VoteStatus {
Pass, // vote has been successful
Fail, // vote has failed due to disagreement in votes
Pending, // not enough people have voted
}
// Contract storage
#[ink(storage)]
pub struct Prosopo {
providers: Mapping<AccountId, Provider>,
provider_accounts: Mapping<ProviderState, BTreeSet<AccountId>>,
service_origins: Mapping<Hash, ()>,
captcha_data: Mapping<Hash, CaptchaData>,
provider_stake_default: Balance,
dapp_stake_default: Balance,
dapps: Mapping<AccountId, Dapp>,
dapp_accounts: Lazy<Vec<AccountId>>,
operators: Mapping<AccountId, Operator>,
operator_accounts: Lazy<Vec<AccountId>>,
operator_stake_default: Balance,
captcha_solution_commitments: Mapping<Hash, CaptchaSolutionCommitment>, // the commitments submitted by DappUsers
dapp_users: Mapping<AccountId, User>,
dapp_user_accounts: Lazy<Vec<AccountId>>,
operator_votes: Mapping<AccountId, Vote>,
max_user_history_len: u16, // the max number of captcha results to store in history for a user
max_user_history_age: u64, // the max age of captcha results to store in history for a user
min_num_active_providers: u16, // the minimum number of active providers required to allow captcha services
max_provider_fee: Balance,
}
/// The Prosopo error types
///
#[derive(Default, PartialEq, Debug, Eq, Clone, Copy, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))]
pub enum Error {
/// Returned if minimum number of operators is not supplied
MinimumTwoOperatorsRequired,
/// Returned if calling account is not authorised to perform action
NotAuthorised,
/// Returned if not enough contract balance to fulfill a request is available.
ContractInsufficientFunds,
/// Returned when the contract to address transfer fails
ContractTransferFailed,
/// Returned if provider exists when it shouldn't
ProviderExists,
/// Returned if provider does not exist when it should
ProviderDoesNotExist,
/// Returned if provider has insufficient funds to operate
ProviderInsufficientFunds,
/// Returned if provider is inactive and trying to use the service
ProviderInactive,
/// Returned if service_origin is already used by another provider
ProviderServiceOriginUsed,
/// Returned if requested captcha data id is unavailable
DuplicateCaptchaDataId,
/// Returned if dapp exists when it shouldn't
DappExists,
/// Returned if dapp does not exist when it should
DappDoesNotExist,
/// Returned if dapp is inactive and trying to use the service
DappInactive,
/// Returned if dapp has insufficient funds to operate
DappInsufficientFunds,
/// Returned if captcha data does not exist
CaptchaDataDoesNotExist,
/// Returned if solution commitment does not exist when it should
CaptchaSolutionCommitmentDoesNotExist,
/// Returned if solution commitment already exists when it should not
CaptchaSolutionCommitmentExists,
/// Returned if dapp user does not exist when it should
DappUserDoesNotExist,
/// Returned if there are no active providers
NoActiveProviders,
/// Returned if the dataset ID and dataset ID with solutions are identical
DatasetIdSolutionsSame,
/// Returned if the captcha solution commitment has already been approved
CaptchaSolutionCommitmentAlreadyApproved,
/// Returned if the captcha solution commitment has already been approved
CaptchaSolutionCommitmentAlreadyDisapproved,
/// Returned if the caller has set their own AccountId as the code hash
InvalidCodeHash,
/// CodeNotFound ink env error
CodeNotFound,
/// An unknown ink env error has occurred
#[default]
Unknown,
/// Invalid contract
InvalidContract,
/// Invalid payee. Returned when the payee value does not exist in the enum
InvalidPayee,
/// Returned if not all captcha statuses have been handled
InvalidCaptchaStatus,
/// No correct captchas in history (either history is empty or all captchas are incorrect)
NoCorrectCaptcha,
/// Returned if the function has been disabled in the contract
FunctionDisabled,
/// Returned if not enough providers are active
NotEnoughActiveProviders,
/// Returned if provider fee is too high
ProviderFeeTooHigh,
/// Returned if the account is an operator, hence the operation is not allowed due to conflict of interest
AccountIsOperator,
/// Returned if the signature is invalid during signing
InvalidSignature,
/// Returned if the public key is invalid during signing
InvalidPublicKey,
/// Returned if the captcha solution commitment is not pending, i.e. has already been dealt with
CaptchaSolutionCommitmentNotPending,
/// Returned if the commitment already exists
CaptchaSolutionCommitmentAlreadyExists,
}
impl Prosopo {
/// Constructor
#[ink(constructor, payable)]
pub fn default(
operator_accounts: Vec<AccountId>,
provider_stake_default: Balance,
dapp_stake_default: Balance,
max_user_history_len: u16,
max_user_history_age: u64,
min_num_active_providers: u16,
max_provider_fee: Balance,
) -> Self {
if operator_accounts.len() < 2 {
panic!("{:?}", Error::MinimumTwoOperatorsRequired)
}
let mut operators = Mapping::new();
let mut operator_accounts_lazy = Lazy::new();
for operator_account in operator_accounts.iter() {
let operator = Operator {
status: GovernanceStatus::Active,
};
operators.insert(operator_account, &operator);
operator_accounts_lazy.set(&operator_accounts);
}
Self {
providers: Default::default(),
provider_accounts: Default::default(),
service_origins: Default::default(),
captcha_data: Default::default(),
operator_accounts: operator_accounts_lazy,
operator_stake_default: 0,
dapp_users: Default::default(),
operators,
provider_stake_default,
dapp_stake_default,
dapps: Default::default(),
dapp_accounts: Default::default(),
dapp_user_accounts: Default::default(),
operator_votes: Default::default(),
max_user_history_len,
max_user_history_age,
captcha_solution_commitments: Default::default(),
min_num_active_providers,
max_provider_fee,
}
}
/// Verify a signature. The payload is a blake128 hash of the payload wrapped in the Byte tag. E.g.
/// message="hello"
/// hash=blake128(message) // 0x1234... (32 bytes)
/// payload="<Bytes>0x1234...</Bytes>" (32 bytes + 15 bytes (tags) + 2 bytes (multihash notation) = 49 bytes)
///
/// Read more about multihash notation here https://w3c-ccg.github.io/multihash/index.xml#mh-example (adds two bytes to identify type and length of hash function)
///
/// Note the signature must be sr25519 type.
#[ink(message)]
pub fn verify_sr25519(
&self,
signature: [u8; 64],
payload: [u8; 49],
) -> Result<bool, Error> {
let caller = self.env().caller();
let mut caller_bytes = [0u8; 32];
let caller_ref: &[u8] = caller.as_ref();
caller_bytes.copy_from_slice(&caller_ref[..32]);
debug!("caller {:?}", caller);
debug!("sig {:?}", signature);
debug!("payload {:?}", payload);
// let sig = Signature::from_bytes(&signature).map_err(|_| Error::InvalidSignature)?;
// let pub_key =
// PublicKey::from_bytes(&caller_bytes).map_err(|_| Error::InvalidPublicKey)?;
// let res = pub_key.verify_simple(crate::CTX, &payload, &sig);
// Ok(res.is_ok())
let res = self.env().sr25519_verify(&signature, &payload, &caller_bytes);
Ok(res.is_ok())
}
#[ink(message)]
pub fn get_caller(&self) -> AccountId {
debug!("caller: {:?}", self.env().caller());
self.env().caller()
}
/// Print and return an error
fn print_err(&self, err: Error, fn_name: &str) -> Error {
debug!(
"ERROR in {}() at block {} with caller {:?}\n'{:?}'",
fn_name,
self.env().block_number(),
self.env().caller(),
err
);
err
}
/// Get contract provider minimum stake default.
#[ink(message)]
pub fn get_provider_stake_default(&self) -> Balance {
self.provider_stake_default
}
/// Get contract dapp minimum stake default.
#[ink(message)]
pub fn get_dapp_stake_default(&self) -> Balance {
self.dapp_stake_default
}
/// Convert a vec of u8 into a Hash
fn hash_vec_u8(&self, data: &Vec<u8>) -> Hash {
let slice = data.as_slice();
let mut hash_output = <Blake2x256 as HashOutput>::Type::default();
<Blake2x256 as CryptoHash>::hash(slice, &mut hash_output);
Hash::from(hash_output)
}
fn check_provider_fee(&self, fee: u32) -> Result<(), Error> {
if fee as u128 > self.max_provider_fee {
return err!(Error::ProviderFeeTooHigh);
}
Ok(())
}
/// Register a provider, their service origin and fee
#[ink(message)]
pub fn provider_register(
&mut self,
service_origin: Vec<u8>,
fee: u32,
payee: Payee,
) -> Result<(), Error> {
let provider_account = self.env().caller();
// this function is for registration only
if self.providers.get(provider_account).is_some() {
return err!(Error::ProviderExists);
}
self.check_provider_fee(fee)?;
// provider cannot be an operator
self.check_not_operator(provider_account)?;
let service_origin_hash = self.hash_vec_u8(&service_origin);
// prevent duplicate service origins
if self.service_origins.get(service_origin_hash).is_some() {
return err!(Error::ProviderServiceOriginUsed);
}
let balance: Balance = 0;
// add a new provider
let provider = Provider {
status: GovernanceStatus::Deactivated,
balance,
fee,
service_origin,
dataset_id: Hash::default(),
payee,
};
self.providers.insert(provider_account, &provider);
self.service_origins.insert(service_origin_hash, &());
let mut provider_accounts_map = self
.provider_accounts
.get(ProviderState {
status: GovernanceStatus::Deactivated,
payee,
})
.unwrap_or_default();
provider_accounts_map.insert(provider_account);
self.provider_accounts.insert(
ProviderState {
status: GovernanceStatus::Deactivated,
payee,
},
&provider_accounts_map,
);
Ok(())
}
/// Update an existing provider, their service origin, fee and deposit funds
#[ink(message)]
#[ink(payable)]
pub fn provider_update(
&mut self,
service_origin: Vec<u8>,
fee: u32,
payee: Payee,
) -> Result<(), Error> {
let provider_account = self.env().caller();
// this function is for updating only, not registering
if self.providers.get(provider_account).is_none() {
return err!(Error::ProviderDoesNotExist);
}
self.check_provider_fee(fee)?;
// provider cannot be an operator
self.check_not_operator(provider_account)?;
let existing = self.get_provider_details(provider_account)?;
if existing.service_origin != service_origin {
let service_origin_hash = self.hash_vec_u8(&service_origin);
// prevent duplicate service origins
if self.service_origins.get(service_origin_hash).is_some() {
return err!(Error::ProviderServiceOriginUsed);
} else {
let existing_service_origin_hash = self.hash_vec_u8(&existing.service_origin);
self.service_origins.remove(existing_service_origin_hash);
self.service_origins.insert(service_origin_hash, &());
}
}
let old_status = existing.status;
let mut new_status = existing.status;
let balance = existing.balance + self.env().transferred_value();
if balance >= self.provider_stake_default && existing.dataset_id != Hash::default() {
new_status = GovernanceStatus::Active;
}
// update an existing provider
let provider = Provider {
status: new_status,
balance,
fee,
service_origin,
dataset_id: existing.dataset_id,
payee,
};
self.provider_change_status(provider_account, old_status, new_status, payee);
self.providers.insert(provider_account, &provider);
Ok(())
}
/// Switch the `provider_account` between indexes in `self.provider_accounts`
fn provider_change_status(
&mut self,
provider_account: AccountId,
current_provider_status: GovernanceStatus,
new_status: GovernanceStatus,
payee: Payee,
) {
if current_provider_status != new_status {
let current_key = ProviderState {
status: current_provider_status,
payee,
};
let new_key = ProviderState {
status: new_status,
payee,
};
// Retrieve indexes from storage mapping
let mut current_status_provider_accounts =
self.provider_accounts.get(current_key).unwrap_or_default();
let mut new_status_provider_accounts =
self.provider_accounts.get(new_key).unwrap_or_default();
// Move provider to the correct index
current_status_provider_accounts.remove(&provider_account);
new_status_provider_accounts.insert(provider_account);
// Store mapping
self.provider_accounts
.insert(current_key, ¤t_status_provider_accounts);
self.provider_accounts
.insert(new_key, &new_status_provider_accounts);
}
}
/// De-Register a provider by setting their status to Deactivated
#[ink(message)]
pub fn provider_deregister(&mut self, provider_account: AccountId) -> Result<(), Error> {
let caller = self.env().caller();
if caller != provider_account {
return err!(Error::NotAuthorised);
}
// Get provider
let mut provider = self
.providers
.get(provider_account)
.ok_or_else(err_fn!(Error::ProviderDoesNotExist))?;
// Update provider status
self.provider_change_status(
provider_account,
provider.status,
GovernanceStatus::Deactivated,
provider.payee,
);
provider.status = GovernanceStatus::Deactivated;
self.providers.insert(provider_account, &provider);
Ok(())
}
/// Unstake and deactivate the provider's service, returning stake
#[ink(message)]
#[ink(payable)]
pub fn provider_unstake(&mut self) -> Result<(), Error> {
let caller = self.env().caller();
if self.providers.get(caller).is_none() {
return err!(Error::ProviderDoesNotExist);
}
let provider = self.get_provider_details(caller)?;
let balance = provider.balance;
if balance > 0 {
self.env()
.transfer(caller, balance)
.map_err(|_| Error::ContractTransferFailed)?;
self.provider_deregister(caller)?;
}
Ok(())
}
/// Add a new data set
#[ink(message)]
pub fn provider_add_dataset(
&mut self,
dataset_id: Hash,
dataset_id_content: Hash,
) -> Result<(), Error> {
if dataset_id == dataset_id_content {
return err!(Error::DatasetIdSolutionsSame);
}
let provider_id = self.env().caller();
// the calling account must belong to the provider
self.validate_provider_exists_and_has_funds(provider_id)?;
let dataset = CaptchaData {
provider: provider_id,
dataset_id,
dataset_id_content,
captcha_type: CaptchaType::ImageGrid,
};
let mut provider = self
.providers
.get(provider_id)
.ok_or_else(err_fn!(Error::ProviderDoesNotExist))?;
let dataset_id_old = provider.dataset_id;
// create a new id and insert details of the new captcha data set if it doesn't exist
if self.captcha_data.get(dataset_id).is_none() {
self.captcha_data.insert(dataset_id, &dataset);
// remove the old dataset_id from the Mapping
self.captcha_data.remove(dataset_id_old);
}
// set the captcha data id on the provider
provider.dataset_id = dataset_id;
let old_status = provider.status;
// change the provider status to active if it was not active
if provider.status != GovernanceStatus::Active
&& provider.balance >= self.provider_stake_default
&& dataset_id != Hash::default()
{
provider.status = GovernanceStatus::Active;
self.provider_change_status(
provider_id,
old_status,
provider.status,
provider.payee,
);
}
self.providers.insert(provider_id, &provider);
Ok(())
}
/// Check the contract is a contract
fn check_is_contract(&self, contract: AccountId) -> Result<(), Error> {
if !self.env().is_contract(&contract) {
return err!(Error::InvalidContract);
}
Ok(())
}
/// Get an existing dapp
fn get_dapp(&self, contract: AccountId) -> Result<Dapp, Error> {
self.dapps
.get(contract)
.ok_or_else(err_fn!(Error::DappDoesNotExist))
}
/// Check a dapp is missing / non-existent
fn check_dapp_does_not_exist(&self, contract: AccountId) -> Result<(), Error> {
if self.dapps.get(contract).is_some() {
return err!(Error::DappExists);
}
Ok(())
}
/// Check a dapp is owned by the caller
fn check_dapp_owner_is_caller(&self, contract: AccountId) -> Result<(), Error> {
let caller = self.env().caller();
let dapp = self.get_dapp(contract)?;
if dapp.owner != caller {
return err!(Error::NotAuthorised);
}
Ok(())
}
/// Configure a dapp's funds and status, handling transferred value
fn dapp_configure_funding(&self, dapp: &mut Dapp) {
// update the dapp funds
dapp.balance += self.env().transferred_value();
// update the dapp status
dapp.status = if dapp.balance >= self.dapp_stake_default {
GovernanceStatus::Active
} else {
GovernanceStatus::Suspended
};
}
/// Configure a dapp (existing or new)
fn dapp_configure(
&mut self,
contract: AccountId,
payee: DappPayee,
owner: AccountId,
) -> Result<Dapp, Error> {
self.check_is_contract(contract)?;
let dapp_lookup = self.dapps.get(contract);
let new = dapp_lookup.is_none();
let mut dapp = dapp_lookup.unwrap_or(Dapp {
owner,
balance: 0,
status: GovernanceStatus::Suspended,
payee,
min_difficulty: 1,
});
// check current contract for ownership
if !new {
self.check_dapp_owner_is_caller(contract)?;
}
dapp.payee = payee; // update the dapp payee
dapp.owner = owner; // update the owner
// owner of the dapp cannot be an operator
self.check_not_operator(owner)?;
self.dapp_configure_funding(&mut dapp);
// if the dapp is new then add it to the list of dapps
if new {
lazy_push!(self.dapp_accounts, contract);
}
// update the dapp in the mapping
self.dapps.insert(contract, &dapp);
Ok(dapp)
}
/// Register a dapp
#[ink(message)]
pub fn dapp_register(
&mut self,
contract: AccountId,
payee: DappPayee,
) -> Result<(), Error> {
// expect dapp to be new
self.check_dapp_does_not_exist(contract)?;
// configure the new dapp
let _dapp = self.dapp_configure(
contract,
payee,
self.env().caller(), // the caller is made the owner of the contract
)?;
Ok(())
}
/// Update a dapp with new funds, setting status as appropriate
pub fn dapp_update(
&mut self,
contract: AccountId,
payee: DappPayee,
owner: AccountId,
) -> Result<(), Error> {
// expect dapp to exist
self.get_dapp(contract)?;
// configure the new dapp
let _dapp = self.dapp_configure(contract, payee, owner)?;
Ok(())
}
/// Fund dapp account to pay for services, if the Dapp caller is registered in self.dapps
#[ink(message)]
#[ink(payable)]
pub fn dapp_fund(&mut self, contract: AccountId) -> Result<(), Error> {
let mut dapp = self.get_dapp(contract)?;
// configure funds and status of the dapp
self.dapp_configure_funding(&mut dapp);
// update the dapp in the mapping
self.dapps.insert(contract, &dapp);
Ok(())
}
/// Cancel services as a dapp, returning remaining tokens
#[ink(message)]
pub fn dapp_cancel(&mut self, contract: AccountId) -> Result<(), Error> {
let mut dapp = self.get_dapp(contract)?;
// check current contract for ownership
self.check_dapp_owner_is_caller(contract)?;
let balance = dapp.balance;
if balance > 0 {
self.env()
.transfer(dapp.owner, balance)
.map_err(|_| Error::ContractTransferFailed)?;
}
dapp.status = GovernanceStatus::Deactivated;
dapp.balance = 0;
self.dapps.insert(contract, &dapp);
Ok(())
}
fn check_not_operator(&self, operator: AccountId) -> Result<(), Error> {
if self.operators.get(operator).is_some() {
return err!(Error::AccountIsOperator);
}
Ok(())
}
/// Trim the user history to the max length and age.
/// Returns the history and expired hashes.
fn trim_user_history(&self, mut history: Vec<Hash>) -> (Vec<Hash>, Vec<Hash>) {
// note that the age is based on the block timestamp, so calling this method as blocks roll over will result in different outcomes as the age threshold will change but the history will not (assuming no new results are added)
let block_timestamp = self.env().block_timestamp();
let max_age = if block_timestamp < self.max_user_history_age {
block_timestamp
} else {
self.max_user_history_age
};
let age_threshold = block_timestamp - max_age;
let mut expired = Vec::new();
// trim the history down to max length
while history.len() > self.max_user_history_len.into() {
let hash = history.pop().unwrap();
expired.push(hash);
}
// trim the history down to max age
while !history.is_empty()
&& self
.captcha_solution_commitments
.get(history.last().unwrap())
.unwrap()
.completed_at
< age_threshold
{
let hash = history.pop().unwrap();
expired.push(hash);
}
(history, expired)
}
/// Record a captcha result against a user, clearing out old captcha results as necessary.
/// A minimum of 1 captcha result will remain irrelevant of max history length or age.
fn record_commitment(
&mut self,
account: AccountId,
hash: Hash,
result: CaptchaSolutionCommitment,
) {
let mut user = self
.dapp_users
.get(account)
.unwrap_or_else(|| self.create_new_dapp_user(account));
// add the new commitment
self.captcha_solution_commitments.insert(hash, &result);
user.history.insert(0, hash);
// trim the user history by len and age, removing any expired commitments
let (history, expired) = self.trim_user_history(user.history);
// update the user history to the in age / length window set of commitment hashes
user.history = history;
// remove the expired commitments
for hash in expired.iter() {
self.captcha_solution_commitments.remove(hash);
}
self.dapp_users.insert(account, &user);
}
fn get_user_history_summary(
&self,
account: AccountId,
) -> Result<UserHistorySummary, Error> {
let user = self.get_dapp_user(account)?;
let (history, _expired) = self.trim_user_history(user.history);
let mut summary = UserHistorySummary {
correct: 0,
incorrect: 0,
score: 0,
};
for hash in history.iter() {
let result = self.captcha_solution_commitments.get(hash).unwrap();