Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.


Status

Status
titleAccepted

Stakeholders
Outcome

Jira
serverHyperledger JIRA
serverId6326cb0b-65b2-38fd-a82c-67a89277103b
keyIR-934

Due date
Owner

Background

...

Iroha Special Instructions and Iroha Queries processing requires to have a permissions based security model + a way to store some "triggers" on the block chain.

Whitepaper and Iroha v1 documentation were researched. The proposal is to use already existing Iroha Special Instructions + Assets mechanisms for Permissions implementation. 

Introduction

Whitepaper gave some information about https://github.com/hyperledger/iroha/blob/iroha2-dev/iroha_2_whitepaper.md#251-event-listeners, and the need to trigger them based on time, block or "condition".

Problem

The White Paper requires https://github.com/hyperledger/iroha/blob/iroha2-dev/iroha_2_whitepaper.md#211-data-permissions require protection of data from unauthorized read and write cases.access.

Iroha 1 documentationhttps://iroha.readthedocs.io/en/master/concepts_architecture/glossary.html#permission gives a Permission's definition:

A named rule that gives the privilege to perform a command. Permission cannot be granted to an account directly, instead, account has roles, which are collections of permissions. Although, there is an exception, see Grantable Permission.

...

Only grantable permission is given to anaccountdirectlyan account directly. An account that holds grantable permission is allowed to perform some particular action on behalf of another account. For example, if accountaaccount a@domain1gives domain1 gives the accountbaccount b@domain2a domain2 a permission that it can transfer assets — thenbthen b@domain2can domain2 can transfer assets ofaof a@domain1to domain1 to anyone.

As you can see permissions were a first-level entities in Iroha v1 while they can be easily implemented by Iroha Special Instructions.

No Format
pub mod isi {
	...
	enum Instruction {
		Add(...),
		Register(...),
		...
		Check(permissions::isi::CheckInstruction),
	}
}

pub mod permissions::isi {
	pub struct CheckInstruction<C, O> {
		condition: C,
		object: O,
	}
}

Signature of `Instruction::execute` method provides an ability to return error result which for `Check` variant will prevent execution of Instructions for which account has no permissions. Each OOB Instruction and Query will include `Check` instructions in it:

No Format
impl Register {
	fn execute(...) -> Result<...> {
		Check{|account| account.asset("role") == ADMIN }.execute(...)?;
		...
	}
}

In this example you can see that we do not need to add permission related attributes to the `Account` and can use assets instead.

The same can be done with custom permissions, storing them in assets components of the account.

This whole concept can be used for event listeners, `Instruction::Listener` can be executed for their configuration, but additionally to storing them in assets components of the account we need to find a way to store global Listeners (Domain level, Peer level) and define cases where we need time based listeners.

"Listeners" store

Current design assumes that we provide instructions with a mutable reference to `WorldStateView` after their validation and store on the block chain. So the only way to trigger Listeners of Conditions (changes in World State View) and Blocks and, possibly, Time is to store them in `WorldStateView`:

No Format
  6 /// Current state of the blockchain alligned with `Iroha` module.
  7 #[derive(Debug, Clone)]
  8 pub struct WorldStateView {
  9     peer: Peer,
        listeners: Vec<ListenerInstruction>,
 10 }
 11 
 12 impl WorldStateView {
 13     /// Default `WorldStateView` constructor.
 14 +---  3 lines: pub fn new(peer: Peer) -> Self {··························································································································
 17 
 18     /// Put `ValidBlock` of information with changes in form of **Iroha Special Instructions**
 19     /// into the world.
 20     pub async fn put(&mut self, block: &CommittedBlock) {
 21         for transaction in &block.transactions {
 22             if let Err(e) = &transaction.proceed(self) {
 23                 eprintln!("Failed to procced transaction on WSV: {}", e);
 24             }
 25         }
 26     }

As you can see - `put` method may invoke (trigger) listeners execution (because they are just a set of out of the box instructions). The question is - should we execute them here or send a new transaction to the ledger?

ASAP execution

Let's imagine as soon as possible execution and consequences:

No Format
 12 impl WorldStateView {
 13     /// Default `WorldStateView` constructor.
 14 +---  3 lines: pub fn new(peer: Peer) -> Self {··························································································································
 17 
 18     /// Put `ValidBlock` of information with changes in form of **Iroha Special Instructions**
 19     /// into the world.
 20     pub async fn put(&mut self, block: &CommittedBlock) {
 21         for transaction in &block.transactions {
 22             if let Err(e) = &transaction.proceed(self) {
 23                 eprintln!("Failed to procced transaction on WSV: {}", e);
 24             }
                for listener in self.listeners {
                  listener.execute(self);
                }
 25         }
 26     }

First question - how should listener understand was it triggered or not? It has only current state, not the delta of changes.

Second one - how should we spread results of it's execution between peers and synchronize I/O actions like notifications, etc.?

Third one - how should we check that listener execution will not break next transactions?

This questions gave us following requirements:

  • Listeners should be executed only once in the ledger
  • Listeners should be able to track changes (in time, blockchain and world_state_view)
  • Listeners should be not executed during validation phase (to prevent unexpected I/O)
  • Listeners should be executed after all block's transactions execution

Follow-up execution

What if instead of asap execution we will only form a new transaction with a set of instructions to execute?

No Format
 18     /// Put `ValidBlock` of information with changes in form of **Iroha Special Instructions**
 19     /// into the world.
 20     pub async fn put(&mut self, block: &CommittedBlock) {
 21         for transaction in &block.transactions {
 22             if let Err(e) = &transaction.proceed(self) {
 23                 eprintln!("Failed to procced transaction on WSV: {}", e);
 24             }
 25         }
            client.submit_all(
                listeners.execute(self, block)
            );
 26     }

But almost all requirements stay open with this solution.

The problematic

The main problem is how Iroha Special Instructions with only World State View as input can be triggered by World State View changes and where to store such Iroha Special Instructions.

World State View by itself held all information needed for the triggering but the current ISI model doesn't provide a way to use them instead of "hardcoded" entities.

If we will be able to make new Out of the Box set of ISI - `ListenerInstruction` with possible variants like `On{trigger: Box<Instruction>, generators: Box<Fn(?)→[Instruction]}` we will be able to store them on the Peer and execute on every `WorldStateView:put` which will result in a new set of generated instructions. This brings us to the next question - what this triggered instructions should know about their triggers - if nothing than we are good with current `ISI::execute` signature. But if we need to pass some information, we should store this info. And to do not change good (in my humble opinion) signature of `ISI::execute` we should store this trigger-related information in generated Instructions. Which means that for cases where we need this information we need special Iroha Special Instructions =)

Let's try to simplify these thoughts:

No Format
BLOCK --> WorldStateView
WorldStateView --check triggers & generate [ISI]--> WorldStateView::Vec<ListenerInstruction>
WorldStateView --execute instructions--> BLOCK
WorldStateView --send new transaction--> Iroha

This approach fits almost all our requirements, the problem is to not send new transactions on validation stage and prevent execution of I\O related instructions multiple times.

The second one should be moved into separate RFC because it also implied to regular ISI.

The first one should be addressed here.

1 and had a strict hardcoded verification logic. Iroha1 was mainly planned to be used as a private blockchain and this system would work there. Yet Iroha2 is planned to be used in both private and public blockchains and therefore needs some degree of customization on how permissions are checked to implement some more complex cases.

Solution

As the Iroha project progresses towards being able to be used in different types of blockchains. It would be good to give more powers to developers. Therefore this RFC introduces a minimal Permission framework, which let's the developers of each blockchain have the power to set up their permission checks accordingly. Of course there will be an out of box implementations provided which can be used for simple blockchains, for more complex logic the developers will be able to define their completely own Permission checks implementation that would suit their needs.

TL;DR

  • Introduce Permission Checker trait
  • Make Iroha generic over `PermissionChecker` and `Permission` types
  • Implement out of box `IrohaPermissionChecker` with minimal checks and the ability to tweak it through the `IrohaPermissionCheckerBuilder`

Code Example


trait PermissionChecker<Permission> {
    pub fn check_permissions(instruction: ISI, authority: Id) -> Result<(), String>
}

Register<Permission, Account>: ISI #[derive(Encode, Decode, Serialize, Deserialize)] struct Account<Permission> { permissions: Vec<Permission>,
account_data: // .. other fields } struct Iroha<Permission, PC: PermissionChecker<Permission>> { pub checker: PC, // .. other fields }

Iroha<Iroha1Permission, Iroha1PermissionChecker>

enum Iroha1Permission {

}

Pros

  1. Customizable permissions check logic
  2. Customizable permission type
  3. Permission check logic is written in pure rust - which is a turing complete and convenient language while ISIs are not yet there.
  4. Faster than WASM
  5. Does not introduce additional complexity as WASM does.

Cons

  1. Can be customized only at compile time, can not be stored in genesis

Alternatives

  1. Use Iroha 1 approach with roles and grantable permissions and do hardcoded permissions checks inside instructions
    1. Pros:
      1. Tested in already running blockchains like Bakong and Byacco
    2. Cons:
      1. Hardcoded permission model - not possible to suit to different types of blockchains
  2. Use Iroha Special Instructions as scripting for checking permissions + Assets mechanisms to store. With two options: implement permission checks as triggers, or simply another part of validation pipeline.
    1. Pros:
      1. Customizable permissions check logic
      2. Can be changed at runtime and stored in file
    2. Cons:
      1. ISIs and Queries are not mature enough to write complex logic that might be needed for permission checks
      2. Triggers design is not finalized and they are not implemented.
  3. Use WASM permission check functions
    1. Pros:
      1. Customizable permissions check logic
      2. Can be changed at runtime and stored in file
      3. Can be written in any language that can be compiled to WASM
    2. Cons:
      1. WASM execution is in general slower
      2. Types through all the codebase will need to be adapted to be compatible with WASM
      3. More research about the WASM libraries and execution is needed

Additional Information

  • This solution impacts the Genesis Block design
  • This makes Iroha closer to a framework

...