This is a simple Substrate runtime module to store online distributed proof of existence for any file.
Our first Substrate runtime module will allow users & applications to store & retrieve digital fingerprint of files. This is a process known as Anchoring or "Proof-of-existence", as in, in the case of a dispute in a contractual relation or regarding the ownership of an asset, a person is able to prove that a certain document with a specific content existed at a certain point in time therefore "proving" the author's or owner's point.
To implement an anchoring mechanism, our module must be able to:
- store the hash (or digest) of a file's content, submitted as part of a transaction.
- allow to retrieve a hash, to verify its existence on chain.
To keep things simple, no fees are involved at this stage (this will be for the level-2 step of the tutorial).
In this first step, there are 2 ways to do it:
- Quick way: clone this repository's level-0 branch to get started directly. You can then skip ahead to point 2.
- Recommended way: Start fresh from the substrate-node-template repository. Follow the instructions below.
Clone the substrate-node-template repo into a local folder:
git clone --depth=1 https://github.com/substrate-developer-hub/substrate-node-template.git my_project
cd my_projectUse the bash script to rename the template to your project name:
chmod u+x ./substrate-node-rename.sh
./substrate-node-rename.sh "my_project" "author"This should give you the following output:
Moving project folder...
Customizing project...
Rename CompleteThe template has now been renamed to your very own project name, and we can get to work !
Optionally, but recommended: Move things around a bit & create a fresh git repo:
cd my_project
rm LICENSE README.md substrate-node-rename.sh substrate-node-template.tar.gz
mv my_project/* ./ && rm -r my_project/ && rm -rf .git/
git init . && git add . && git commit -a -m "Initial commit"Now that you've got a clean runtime module project, let's first make sure you can build it:
Ensure your have the required dependencies installed on your machine (Rust, etc.):
curl https://sh.rustup.rs -sSf | sh
Install required tools
./scripts/init.shBuild the project (Was and native code)
cargo buildTo start a single-node development chain:
cargo run — —devAs part of the template, we have a generic module located at runtime/src/template.rs
Let's modify it to build our own proof-of-existence runtime module. First, we'll rename the module:
- Rename runtime/src/template.rs to runtime/src/poe.rs:
mv runtime/src/template.src runtime/src/poe.rs- In runtime/src/lib.rs, perform the following replacements:
//REMOVE THIS: mod template;
mod poe;
//...
//REMOVE THIS: impl template::Trait for Runtime {
impl poe::Trait for Runtime {
//...
}
construct_runtime!(
//...
//REMOVE THIS: TemplateModule: template::{Module, Call, Storage, Event<T>},
Poe: poe::{Module, Call, Storage, Event<T>},
//...
);If you got it right, the project should still build correctly !
Head over to the runtime/src/poe.rs file that contains our runtime module's code.
Using the decl_storage! macro, we define storage items, that is, data structures that will be stored on-chain. Let's add a map (Substrate's hashtable) to store file fingerprints and their associated owner. For this, we need a StorageMap so don't forget to add it to the use::support instruction above.
use support::{decl_storage, StorageMap};
use rstd::vec::Vec;
pub const ERR_DIGEST_TOO_LONG: &str = "Digest too long (max 100 bytes)";
pub const DIGEST_MAXSIZE: usize = 100;
decl_storage! {
trait Store for Module<T: Trait> as PoeStorage {
// Define a 'Proofs' storage space for a map with
// the proof digest as the key, and associated AccountId as value.
// The 'get(proofs)' is the default getter.
Proofs get(proofs): map Vec<u8> => T::AccountId;
}
}Before proceeding to defining the functions that will implement the logic of our proof-of-existence system, we need to declare the events that will be raised whenever a proof was stored or erased. We add them using the decl_event! macro:
use support::{decl_event};
decl_event!(
pub enum Event<T> where AccountId = <T as system::Trait>::AccountId {
// Event emitted when a proof has been stored into chain storage
ProofStored(AccountId, Vec<u8>),
// Event emitted when a proof has been erased from chain storage
ProofErased(AccountId, Vec<u8>),
}
);Finally we can implement the module's logic, exposed to external apps / systems as dispatchable functions, using the decl_module! macro:
- The store_proof function will record a proof digest on-chain along with the Account that initiated the transaction for the call (after ensuring the call is valid, and the proof is not stored yet).
- The erase_proof function will suppress a pproof digest from on-chain storage, after having verified that the proof is known and the sending Account is effectively the owner of that proof.
use support::{decl_module, ensure, dispatch::Result};
use system::ensure_signed;
// The module's dispatchable functions.
decl_module! {
/// The module declaration.
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// Initializing events
// this is needed only if you are using events in your module
fn deposit_event() = default;
fn store_proof(origin, digest: Vec<u8>) -> Result {
// Verify that the incoming transaction is signed
let sender = ensure_signed(origin)?;
// Validate digest does not exceed a maximum size
ensure!(digest.len() <= DIGEST_MAXSIZE, ERR_DIGEST_TOO_LONG);
// Verify that the specified proof has not been stored yet
ensure!(!Proofs::<T>::exists(&digest), "This proof has already been stored");
// Store the proof and the sender of the transaction
Proofs::<T>::insert(&digest, sender.clone());
// Issue an event to notify that the proof was successfully stored
Self::deposit_event(RawEvent::ProofStored(sender, digest));
Ok(())
}
fn erase_proof(origin, digest: Vec<u8>) -> Result {
// Verify that the incoming transaction is signed
let sender = ensure_signed(origin)?;
// Validate digest does not exceed a maximum size
ensure!(digest.len() <= DIGEST_MAXSIZE, ERR_DIGEST_TOO_LONG);
// Verify that the specified proof has been stored before
ensure!(Proofs::<T>::exists(&digest), "This proof has not been stored yet");
// Get owner associated with the proof
let owner = Self::proofs(&digest);
// Verify that sender of the current tx is the proof owner
ensure!(sender == owner, "You must own this proof to erase it");
// Erase proof from storage
Proofs::<T>::remove(&digest);
// Issue an event to notify that the proof was effectively erased
Self::deposit_event(RawEvent::ProofErased(sender, digest));
Ok(())
}
}
}To verify that the module is building correctly, we first comment out everything enclosed in the mod tests at the bottom of the file,
/* COMMENT EVERYTHING BELOW FOR NOW */
/// tests for this module
//#[cfg(test)]
//mod tests {
// ...
//}and then run:
cargo buildNow that we have implemented our basic proof-of-existence runtime module, we will write unit tests to verify the logic written in the module's dispatchable functions behave as expected. (First uncomment the lines we previously commented out ...)
#[cfg(test)]
mod tests {
use support::{impl_outer_origin, assert_ok, assert_noop, parameter_types};
// ...
//REMOVE THIS: type TemplateModule = Module<Test>;
type POEModule = Module<Test>;
// ...
#[test]
fn it_works() {
with_externalities(&mut new_test_ext(), || {
// Verify it's not possible to store exceedingly big digests (prevent DOS attack and/or chain storage bloat)
assert_noop!(POEModule::store_proof(Origin::signed(1), vec![0; 101]), "Digest too long (max 100 bytes)");
// Have account 1 stores a proof
assert_ok!(POEModule::store_proof(Origin::signed(1), vec![0]));
// Check that account 2 cannot create the same proof
assert_noop!(POEModule::store_proof(Origin::signed(2), vec![0]), "This proof has already been stored");
// Check that account 2 cannot erase a proof they do not own
assert_noop!(POEModule::erase_proof(Origin::signed(2), vec![0]), "You must own this proof to erase it");
// Check that account 2 cannot revoke some non-existent proof
assert_noop!(POEModule::erase_proof(Origin::signed(2), vec![1]), "This proof has not been stored yet");
// Check that account 1 can erase their proof
assert_ok!(POEModule::erase_proof(Origin::signed(1), vec![0]));
// Check that account 2 can now store this proof
assert_ok!(POEModule::store_proof(Origin::signed(2), vec![0]));
});
}
}
To execute the tests, either run:
cargo test -p my-project-runtime templateor
cd runtime
cargo testIf all goes well, you should get the following output:
running 1 test
test poe::tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outIf you successfully reached this point, congratulations, you just wrote your first fully-functional Substrate runtime module !!!
You can now proceed to level-1 of the tutorial.