Skip to content

Commit 441c912

Browse files
authored
Merge pull request #92 from docknetwork/public-attestation
public attestation on-chain module
2 parents cd47663 + 346e1ad commit 441c912

File tree

5 files changed

+405
-2
lines changed

5 files changed

+405
-2
lines changed

Diff for: runtime/src/attest.rs

+379
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
//! This module allows DIDs to publically attests to arbirary (and arbitrarily large) RDF
2+
//! claimgraphs. These attestations are not stored on-chain; rather, the attester chooses a storage
3+
//! method by specifying an Iri.
4+
5+
use crate::did::{self, Did, DidSignature};
6+
use crate::StateChange;
7+
use alloc::vec::Vec;
8+
use codec::{Decode, Encode};
9+
use frame_support::{
10+
decl_error, decl_module, decl_storage, dispatch::DispatchResult, ensure, traits::Get,
11+
weights::Weight,
12+
};
13+
use frame_system::{self as system, ensure_signed};
14+
15+
pub type Iri = Vec<u8>;
16+
17+
pub trait Trait: system::Trait + did::Trait {
18+
/// The cost charged by the network to store a single byte in chain-state for the life of the
19+
/// chain.
20+
type StorageWeight: Get<Weight>;
21+
}
22+
23+
#[derive(Encode, Decode, Clone, PartialEq, Debug, Default)]
24+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25+
pub struct Attestation {
26+
#[codec(compact)]
27+
priority: u64,
28+
iri: Option<Iri>,
29+
}
30+
31+
decl_error! {
32+
/// Error for the attest module.
33+
pub enum Error for Module<T: Trait> {
34+
/// The Attestation was not posted because its priority was less than or equal to that of
35+
/// an attestation previously posted by the same entity.
36+
///
37+
/// Note, the default value for an attestation is the null claim (claiming nothing), with
38+
/// a priority of 0. To override this initial value, a priority of 1 or greater is required.
39+
/// Check to see that the provided priority is not zero as that could be the cause of this
40+
/// error.
41+
PriorityTooLow,
42+
/// Signature verification failed while adding blob
43+
InvalidSig
44+
}
45+
}
46+
47+
decl_storage! {
48+
trait Store for Module<T: Trait> as Blob {
49+
// The priority value provides replay protection and also gives attestations a paritial
50+
// ordering. Signatures with lesser or equal priority to those previously posted by the same
51+
// entity are not accepted by the chain.
52+
//
53+
// Notice that priority is not a block-number. This is intentional as it yields some desired
54+
// properties and allows some potential use-cases:
55+
// - When publishing consecutive attestations, the attester need not care at which block a
56+
// a previous attestation was included. This means attestations can be made over a
57+
// mono-directional channel.
58+
// - Timestamps may be used as priority when available.
59+
// - A timeline of future attestations may be constructed by encrypting multiple
60+
// signatures, each with the output of a verifiable delay function. A "final"
61+
// attestation may be selected by assigning it the higest prority in the batch.
62+
// The "final" attestation will be acceptable by the runtime regardless of whether its
63+
// predecessors were submitted.
64+
//
65+
// An attestation on chain with iri set to None is sematically meaningless. Setting the
66+
// iri to None is equivalent to attesting to the empty claimgraph.
67+
//
68+
// When Attestations::get(did).iri == Some(dat) and dat is a valid utf-8 Iri:
69+
// `[did dock:attestsDocumentContents dat]`.
70+
Attestations: map hasher(blake2_128_concat) Did => Attestation;
71+
}
72+
}
73+
74+
decl_module! {
75+
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
76+
#[weight = {
77+
T::DbWeight::get().reads_writes(2, 1)
78+
+ signature.weight()
79+
+ attests
80+
.iri
81+
.as_ref()
82+
.map(|iri| iri.len())
83+
.unwrap_or_default() as Weight
84+
* T::StorageWeight::get()
85+
}]
86+
fn set_claim(
87+
origin,
88+
attester: Did,
89+
attests: Attestation,
90+
signature: DidSignature,
91+
) -> DispatchResult {
92+
Module::<T>::set_claim_(origin, attester, attests, signature)
93+
}
94+
}
95+
}
96+
97+
impl<T: Trait> Module<T> {
98+
fn set_claim_(
99+
origin: <T as system::Trait>::Origin,
100+
attester: Did,
101+
attests: Attestation,
102+
signature: DidSignature,
103+
) -> DispatchResult {
104+
ensure_signed(origin)?;
105+
106+
// check
107+
let payload = StateChange::Attestation((attester, attests.clone())).encode();
108+
let valid = did::Module::<T>::verify_sig_from_did(&signature, &payload, &attester)?;
109+
ensure!(valid, Error::<T>::InvalidSig);
110+
let prev = Attestations::get(&attester);
111+
ensure!(prev.priority < attests.priority, Error::<T>::PriorityTooLow);
112+
113+
// execute
114+
Attestations::insert(&attester, &attests);
115+
116+
Ok(())
117+
}
118+
}
119+
120+
#[cfg(test)]
121+
mod test {
122+
use super::*;
123+
use crate::test_common::*;
124+
use sp_core::sr25519;
125+
126+
type Mod = crate::attest::Module<Test>;
127+
type Er = crate::attest::Error<Test>;
128+
129+
/// Trigger the PriorityTooLow error by submitting a priority 0 attestation.
130+
#[test]
131+
fn priority_too_low() {
132+
ext().execute_with(|| {
133+
let (did, kp) = newdid();
134+
let att = Attestation {
135+
priority: 0,
136+
iri: None,
137+
};
138+
let err = Mod::set_claim(
139+
Origin::signed(0),
140+
did,
141+
att.clone(),
142+
sign(&StateChange::Attestation((did, att)), &kp),
143+
)
144+
.unwrap_err();
145+
assert_eq!(err, Er::PriorityTooLow.into());
146+
});
147+
}
148+
149+
/// assert sizes of encoded Attestation
150+
#[test]
151+
fn encoded_attestation_size() {
152+
ext().execute_with(|| {
153+
for (priority, iri, expected_size) in [
154+
(0, None, 1 + 1),
155+
(63, None, 1 + 1),
156+
(64, None, 2 + 1),
157+
(256, None, 2 + 1),
158+
(0, Some(vec![]), 1 + 2),
159+
(0, Some(vec![0]), 1 + 3),
160+
(0, Some(vec![0; 63]), 1 + 63 + 2),
161+
(0, Some(vec![0; 64]), 1 + 64 + 3),
162+
(0, Some(vec![0; 256]), 1 + 256 + 3),
163+
(63, Some(vec![0; 256]), 1 + 256 + 3),
164+
(64, Some(vec![0; 256]), 2 + 256 + 3),
165+
]
166+
.iter()
167+
.cloned()
168+
{
169+
assert_eq!(Attestation { priority, iri }.encode().len(), expected_size);
170+
}
171+
});
172+
}
173+
174+
/// Trigger the InvalidSig error by tweaking a value in the plaintext after signing
175+
#[test]
176+
fn invalid_sig_a() {
177+
ext().execute_with(|| {
178+
let (dida, kpa) = newdid();
179+
let mut att = Attestation {
180+
priority: 1,
181+
iri: None,
182+
};
183+
let sig = sign(&StateChange::Attestation((dida, att.clone())), &kpa);
184+
att.priority += 1;
185+
let err = Mod::set_claim(Origin::signed(0), dida, att, sig).unwrap_err();
186+
assert_eq!(err, Er::InvalidSig.into());
187+
});
188+
}
189+
190+
/// Trigger the InvalidSig error using a different did for signing
191+
#[test]
192+
fn invalid_sig_b() {
193+
ext().execute_with(|| {
194+
let (dida, _kpa) = newdid();
195+
let (_didb, kpb) = newdid();
196+
let att = Attestation {
197+
priority: 1,
198+
iri: None,
199+
};
200+
let err = Mod::set_claim(
201+
Origin::signed(0),
202+
dida,
203+
att.clone(),
204+
sign(&StateChange::Attestation((dida, att)), &kpb),
205+
)
206+
.unwrap_err();
207+
assert_eq!(err, Er::InvalidSig.into());
208+
});
209+
}
210+
211+
/// Attestations with equal priority are mutually exlusive
212+
#[test]
213+
fn priority_face_off() {
214+
ext().execute_with(|| {
215+
let (did, kp) = newdid();
216+
217+
// same iri
218+
set_claim(
219+
&did,
220+
&Attestation {
221+
priority: 1,
222+
iri: None,
223+
},
224+
&kp,
225+
)
226+
.unwrap();
227+
assert_eq!(
228+
set_claim(
229+
&did,
230+
&Attestation {
231+
priority: 1,
232+
iri: None,
233+
},
234+
&kp,
235+
)
236+
.unwrap_err(),
237+
Er::PriorityTooLow.into()
238+
);
239+
240+
// different iris
241+
set_claim(
242+
&did,
243+
&Attestation {
244+
priority: 2,
245+
iri: Some(vec![0]),
246+
},
247+
&kp,
248+
)
249+
.unwrap();
250+
assert_eq!(
251+
set_claim(
252+
&did,
253+
&Attestation {
254+
priority: 2,
255+
iri: Some(vec![0, 2, 3]),
256+
},
257+
&kp,
258+
)
259+
.unwrap_err(),
260+
Er::PriorityTooLow.into()
261+
);
262+
});
263+
}
264+
265+
/// After attempting a set of attestations the one with highest priority is the one that ends up
266+
/// in chain state.
267+
#[test]
268+
fn priority_battle_royale() {
269+
ext().execute_with(|| {
270+
let (did, kp) = newdid();
271+
let prios: Vec<u64> = (0..200).map(|_| rand::random::<u64>()).collect();
272+
for priority in &prios {
273+
let _ = set_claim(
274+
&did,
275+
&Attestation {
276+
priority: *priority,
277+
iri: None,
278+
},
279+
&kp,
280+
);
281+
}
282+
assert_eq!(
283+
Attestations::get(did).priority,
284+
prios.iter().max().unwrap().clone()
285+
);
286+
});
287+
}
288+
289+
/// An attestation with priority set to the highest value is final.
290+
/// It does not trigger a panic by integer overflow.
291+
#[test]
292+
fn max_priority_is_final() {
293+
ext().execute_with(|| {
294+
let (did, kp) = newdid();
295+
set_claim(
296+
&did,
297+
&Attestation {
298+
priority: u64::max_value(),
299+
iri: None,
300+
},
301+
&kp,
302+
)
303+
.unwrap();
304+
let err = set_claim(
305+
&did,
306+
&Attestation {
307+
priority: u64::max_value(),
308+
iri: None,
309+
},
310+
&kp,
311+
)
312+
.unwrap_err();
313+
assert_eq!(err, Er::PriorityTooLow.into());
314+
});
315+
}
316+
317+
/// Set an attestation that is not None
318+
#[test]
319+
fn set_some_attestation() {
320+
ext().execute_with(|| {
321+
let (did, kp) = newdid();
322+
assert_eq!(
323+
Attestations::get(did),
324+
Attestation {
325+
priority: 0,
326+
iri: None,
327+
}
328+
);
329+
set_claim(
330+
&did,
331+
&Attestation {
332+
priority: 1,
333+
iri: Some(vec![0, 1, 2]),
334+
},
335+
&kp,
336+
)
337+
.unwrap();
338+
assert_eq!(
339+
Attestations::get(did),
340+
Attestation {
341+
priority: 1,
342+
iri: Some(vec![0, 1, 2]),
343+
}
344+
);
345+
});
346+
}
347+
348+
/// Skip a priority value.
349+
#[test]
350+
fn skip_prio() {
351+
ext().execute_with(|| {
352+
let (did, kp) = newdid();
353+
for priority in &[1, 2, 4] {
354+
set_claim(
355+
&did,
356+
&Attestation {
357+
priority: *priority,
358+
iri: None,
359+
},
360+
&kp,
361+
)
362+
.unwrap();
363+
}
364+
});
365+
}
366+
367+
/// helper
368+
fn set_claim(claimer: &did::Did, att: &Attestation, kp: &sr25519::Pair) -> DispatchResult {
369+
Mod::set_claim(
370+
Origin::signed(0),
371+
claimer.clone(),
372+
att.clone(),
373+
sign(
374+
&StateChange::Attestation((claimer.clone(), att.clone())),
375+
kp,
376+
),
377+
)
378+
}
379+
}

Diff for: runtime/src/blob.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub struct Blob {
2828
pub trait Trait: system::Trait + did::Trait {
2929
/// Blobs larger than this will not be accepted.
3030
type MaxBlobSize: Get<u32>;
31+
/// The cost charged by the network to store a single byte in chain-state for the life of the
32+
/// chain.
33+
type StorageWeight: Get<Weight>;
3134
}
3235

3336
decl_error! {
@@ -54,7 +57,8 @@ decl_storage! {
5457
decl_module! {
5558
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
5659
/// Create a new immutable blob.
57-
#[weight = T::DbWeight::get().reads_writes(2, 1) + signature.weight() + (1_100 * blob.blob.len()) as Weight]
60+
#[weight = T::DbWeight::get().reads_writes(2, 1) + signature.weight() +
61+
(blob.blob.len() as Weight * T::StorageWeight::get())]
5862
pub fn new(
5963
origin,
6064
blob: dock::blob::Blob,

0 commit comments

Comments
 (0)