Skip to content
This repository has been archived by the owner on Feb 28, 2021. It is now read-only.

Commit

Permalink
Merge pull request #525 from radicle-dev/versioning
Browse files Browse the repository at this point in the history
Version the state
  • Loading branch information
CodeSandwich committed Jun 17, 2020
2 parents cce95f6 + 20e871a commit 5e5296a
Show file tree
Hide file tree
Showing 17 changed files with 538 additions and 279 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,12 @@ Upcoming

### Breaking changes

* client: `ClientT::get_checkpoint` returns a new `Checkpoint` structure
* runtime: abandon `Checkpoints` storage in favor of `Checkpoints1`
* runtime: abandon `InitialCheckpoints` storage in favor of `InitialCheckpoints1`
* runtime: abandon `Projects` storage in favor of `Projects1`
* runtime: abandon `Users` storage in favor of `Users1`
* runtime: abandon `Orgs` storage in favor of `Orgs1`
* runtime: We introduce the `CheckVersion` transaction validation that requires
authors to include the runtime version in the transaction.
* cli: `project register` now also expects domain type
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 123 additions & 1 deletion client/src/interface.rs
Expand Up @@ -45,6 +45,128 @@ pub type TxHash = Hash;
#[doc(inline)]
pub type BlockHeader = Header;

/// Org
///
/// Different from [state::OrgV1] in which this type gathers
/// both the [`Id`] and the other [`Org`] fields, respectively stored
/// as an Org's storage key and data, into one complete type.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Org {
// Unique id of an Org.
pub id: Id,

/// See [state::OrgV1::account_id]
pub account_id: AccountId,

/// See [state::OrgV1::members]
pub members: Vec<Id>,

/// See [state::OrgV1::projects]
pub projects: Vec<ProjectName>,
}

impl Org {
pub fn new(id: Id, org: state::Orgs1Data) -> Org {
match org {
state::Orgs1Data::V1(org) => Org {
id,
account_id: org.account_id,
members: org.members,
projects: org.projects,
},
}
}
}

/// User
///
/// Different from [state::UserV1] in which this type gathers
/// both the [`Id`] and the other [`User`] fields, respectively stored
/// as an User's storage key and data, into one complete type.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct User {
// Unique id of a User.
pub id: Id,

/// See [state::UserV1::account_id]
pub account_id: AccountId,

/// See [state::UserV1::projects]
pub projects: Vec<ProjectName>,
}

impl User {
pub fn new(id: Id, user: state::Users1Data) -> Self {
match user {
state::Users1Data::V1(user) => Self {
id,
account_id: user.account_id,
projects: user.projects,
},
}
}
}

/// Project
///
/// Different from [state::ProjectV1] in which this type gathers
/// [`ProjectName`], [`ProjectDomain`] and the other [`Project`] fields into one complete type.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Project {
/// The name of the project, unique within its org.
pub name: ProjectName,

/// The domain to which the project belongs.
pub domain: ProjectDomain,

/// See [state::ProjectV1::current_cp]
pub current_cp: CheckpointId,

/// See [state::ProjectV1::metadata]
pub metadata: Bytes128,
}

impl Project {
/// Build a [crate::Project] given all its properties obtained from storage.
pub fn new(name: ProjectName, domain: ProjectDomain, project: state::Projects1Data) -> Self {
match project {
state::Projects1Data::V1(project) => Project {
name,
domain,
current_cp: project.current_cp,
metadata: project.metadata,
},
}
}
}

/// Checkpoint
///
/// Different from [state::CheckpointV1] in which this type gathers
/// both the [`CheckpointId`] and the other [`Checkpoint`] fields, respectively stored
/// as an Checkpoint's storage key and data, into one complete type.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Checkpoint {
/// Checkpoint ID.
pub id: CheckpointId,
/// Previous checkpoint in the project history.
pub parent: Option<CheckpointId>,
/// Hash that identifies a project’s off-chain data.
pub hash: H256,
}

impl Checkpoint {
pub fn new(cp: state::Checkpoints1Data) -> Self {
match cp {
state::Checkpoints1Data::V1(cp) => Self {
id: cp.id(),
parent: cp.parent,
hash: cp.hash,
},
}
}
}

/// Result of a transaction being included in a block.
///
/// Returned after submitting an transaction to the blockchain.
Expand Down Expand Up @@ -144,7 +266,7 @@ pub trait ClientT {

async fn list_projects(&self) -> Result<Vec<ProjectId>, Error>;

async fn get_checkpoint(&self, id: CheckpointId) -> Result<Option<state::Checkpoint>, Error>;
async fn get_checkpoint(&self, id: CheckpointId) -> Result<Option<Checkpoint>, Error>;

async fn onchain_runtime_version(&self) -> Result<RuntimeVersion, Error>;
}
27 changes: 14 additions & 13 deletions client/src/lib.rs
Expand Up @@ -247,35 +247,35 @@ impl ClientT for Client {
}

async fn get_org(&self, id: Id) -> Result<Option<Org>, Error> {
self.fetch_map_value::<registry::store::Orgs, _, _>(id.clone())
self.fetch_map_value::<registry::store::Orgs1, _, _>(id.clone())
.await
.map(|maybe_org: Option<state::Org>| maybe_org.map(|org| Org::new(id, org)))
.map(|maybe_org: Option<state::Orgs1Data>| maybe_org.map(|org| Org::new(id, org)))
}

async fn list_orgs(&self) -> Result<Vec<Id>, Error> {
let orgs_prefix = registry::store::Orgs::final_prefix();
let orgs_prefix = registry::store::Orgs1::final_prefix();
let keys = self.backend.fetch_keys(&orgs_prefix, None).await?;
let mut org_ids: Vec<Id> = Vec::with_capacity(keys.len());
for key in keys {
let org_id = registry::store::Orgs::decode_key(&key)
let org_id = registry::store::Orgs1::decode_key(&key)
.expect("Invalid runtime state key. Cannot extract org ID");
org_ids.push(org_id)
}
Ok(org_ids)
}

async fn get_user(&self, id: Id) -> Result<Option<User>, Error> {
self.fetch_map_value::<registry::store::Users, _, _>(id.clone())
self.fetch_map_value::<registry::store::Users1, _, _>(id.clone())
.await
.map(|maybe_user: Option<state::User>| maybe_user.map(|user| User::new(id, user)))
.map(|maybe_user| maybe_user.map(|user| User::new(id, user)))
}

async fn list_users(&self) -> Result<Vec<Id>, Error> {
let users_prefix = registry::store::Users::final_prefix();
let users_prefix = registry::store::Users1::final_prefix();
let keys = self.backend.fetch_keys(&users_prefix, None).await?;
let mut user_ids: Vec<Id> = Vec::with_capacity(keys.len());
for key in keys {
let user_id = registry::store::Users::decode_key(&key)
let user_id = registry::store::Users1::decode_key(&key)
.expect("Invalid runtime state key. Cannot extract user ID");
user_ids.push(user_id);
}
Expand All @@ -289,28 +289,29 @@ impl ClientT for Client {
project_domain: ProjectDomain,
) -> Result<Option<Project>, Error> {
let project_id = (project_name.clone(), project_domain.clone());
self.fetch_map_value::<registry::store::Projects, _, _>(project_id.clone())
self.fetch_map_value::<registry::store::Projects1, _, _>(project_id.clone())
.await
.map(|maybe_project| {
maybe_project.map(|project| Project::new(project_name, project_domain, project))
})
}

async fn list_projects(&self) -> Result<Vec<ProjectId>, Error> {
let project_prefix = registry::store::Projects::final_prefix();
let project_prefix = registry::store::Projects1::final_prefix();
let keys = self.backend.fetch_keys(&project_prefix, None).await?;
let mut project_ids = Vec::with_capacity(keys.len());
for key in keys {
let project_id = registry::store::Projects::decode_key(&key)
let project_id = registry::store::Projects1::decode_key(&key)
.expect("Invalid runtime state key. Cannot extract project ID");
project_ids.push(project_id);
}
Ok(project_ids)
}

async fn get_checkpoint(&self, id: CheckpointId) -> Result<Option<state::Checkpoint>, Error> {
self.fetch_map_value::<registry::store::Checkpoints, _, _>(id)
async fn get_checkpoint(&self, id: CheckpointId) -> Result<Option<Checkpoint>, Error> {
self.fetch_map_value::<registry::store::Checkpoints1, _, _>(id)
.await
.map(|cp_opt| cp_opt.map(Checkpoint::new))
}

async fn onchain_runtime_version(&self) -> Result<RuntimeVersion, Error> {
Expand Down
5 changes: 1 addition & 4 deletions client/tests/end_to_end.rs
Expand Up @@ -87,10 +87,7 @@ async fn register_project() {
.any(|id| *id == (message.project_name.clone(), message.project_domain.clone()));
assert!(has_project, "Registered project not found in project list");

let checkpoint_ = state::Checkpoint {
parent: None,
hash: project_hash,
};
let checkpoint_ = Checkpoint::new(state::Checkpoints1Data::new(None, project_hash));
let checkpoint = client.get_checkpoint(checkpoint_id).await.unwrap().unwrap();
assert_eq!(checkpoint, checkpoint_);

Expand Down
50 changes: 44 additions & 6 deletions core/README.md
Expand Up @@ -46,21 +46,59 @@ For each entity version the documentation has the following sections

To make the runtime state backwards compatible, every state entity that is added
must be versioned using the following schema.
Please follow the naming convention introduced in the examples as closely as possible.

The storage defines the structure of the data.
If it's altered, e.g. a key type is changed or it's converting from a map to a list,
a new version must be added.
The storage must include its version in its name:

```rust
// registry.rs

pub mod store {
...
decl_storage! {
...
pub Users1: map hasher(blake2_128_concat) Id => Option<state::Users1Data>;
pub Users2: map hasher(blake2_128_concat) NewId => Option<state::Users2Data>;
```

The stored data must be an enum capable of containing different data versions.
It must be versioned consistently and independently of the storage version:

```rust
// state.rs

pub enum User {
UserV1(UserV1)
UserV2(UserV1)
pub enum Users1Data {
V1(UserV1)
V2(UserV2)
}

pub enum Users2Data {
V3(UserId)
V4(UserV4)
}
```

If the stored data is a specialized data structure,
it must be versioned same as the stored data, which contains it:

```rust
// state.rs

pub struct UserV1 {
// ...fields
...
}

pub struct UserV2 {
// ... changed fields
pub enum UserV2 {
...
}

// UserId is general purpose

pub struct UserV4 {
...
}
```

Expand Down

0 comments on commit 5e5296a

Please sign in to comment.