diff --git a/text/003-trigger-authority.md b/text/003-trigger-authority.md new file mode 100644 index 0000000..4f891f4 --- /dev/null +++ b/text/003-trigger-authority.md @@ -0,0 +1,134 @@ +# Refine Triggers Authority Mechanism + +## Context + +Every trigger is composed of: + +- An executable - a set of actions to execute +- An **authority** - an account ID, from under which trigger's actions are executed. +- A filter (or the _kind_ of trigger) - to define a condition where to execute the trigger, one of: + - Pipeline - to react on blocks/transactions + - Time - to execute at a certain time, either scheduled or pre-commit + - Trigger completed - to handle completion of other triggers + - Data - to react on state changes (e.g. domain registered) + - _Execute trigger_ filter, or _by-call_ filter[^1] - states that this trigger is not automatically executed, but only + via `ExecuteTrigger` instruction. + +[^1]: By-call triggers are subject for removal in #5147 + +Currently, the **authority** of the trigger behaves differently depending on the kind of trigger (i.e. on its filter): + +| Trigger Kind | Trigger execution authority | +| -------------------------------------------------------------------------- | ------------------------------------------------------------ | +| Pipeline | Use trigger's | +| Time | Use trigger's | +| Trigger completed | Use trigger's | +| Data | Use caller's (the one who caused the data event) | +| By-call | Use caller's (who executed the `ExecuteTrigger` instruction) | +| Also, if any of the data triggers fail, the entire transaction also fails. | | + +## Motivation to change + +### Use case 1 - deploying on-chain functions callable by less-privileges users but executed with higher privileges + +While building the [Hub Chain PoC](https://github.com/hyperledger-iroha/hub-chain-poc), we try to design an on-chain +agent that would accept an input from an untrusted external entity (a relay), validate it, and perform certain +administrative actions[^2]. + +[^2]: Specifically, the on-chain agent would receive block headers of another chain and proofs that e.g. a transfer + happened there. Then, if the on-chain agent can prove, it would make e.g. a local transfer. + +It would be convenient to register a by-call trigger that would accept transactions from a relay in a form of +`ExecuteTrigger { id: "our-special-trigger", args: ... }`, validate, and perform administrative actions. However, in the +current design the trigger would infer the authority of the caller, i.e. of the relay's account. Therefore, we would +have to give the relay's account sufficient permissions for the trigger to perform these actions. In this case, the +relay could perform these action by itself without an on-chain trust-less validation, which defeats the purpose. + +#### Alternatives + +- **Use Pipeline/Time filters**. These triggers are always executed using the `authority` set in the trigger. Therefore, + they can have administrative permissions, react on some state changes made by an untrusted source, and execute + administrative actions. +- **Use Trigger Completed filter**. Then, an un-trusted source could call some un-trusted trigger that would create some + state change, and then the administrative trigger would execute as a result of that trigger completion, performing + actions with elevated permissions. This works essentially the same as the Pipeline/Time variant, but with immediate + execution. +- **Use executor-level custom instructions**. Executor has "root privileges" and can perform any action. However, it is + not a modular and composable approach, as there is only one global executor in Iroha. With triggers, it is possible + for different unrelated authorities to deploy their own permission-ed logic without interfering with each other. It's + just more modular, come on! + +### Use case 2 + +I (e.g. Alice) want to deploy a data trigger that reacts on someone else's (e.g. Bob's) asset balance change. This +trigger would, for example, make a transfer on _my_ behalf. + +However, when Bob changes his own balance, my trigger would be executed with _Bob's_ permissions, not mine, and +therefore the trigger will fail to make the intended transfer. + +## Proposal + +Current model--always run with caller's authority--is: + +- Maximally safe - a trigger never does more than the caller can +- But useless for efficient privileged automations +- Reduces by-call and data triggers to extensions of the caller's transactions without autonomy + +I see two options. + +### Option 1 - Always run with trigger authority + +- Simple mental model: trigger is an autonomous actor. +- Security: developers must explicitly check caller identity inside the trigger logic. This isn't possible in case of + simple `Executable::Instructions`. + +This is close to how Ethereum smart contracts work, AFAIK. + +### Option 2 - Mixed model + +- Provide an ability to set whether a trigger should use a **pre-defined authority** or the **caller's** authority. +- This would apply to Data, By-Call, and Trigger-Completed triggers. Time and Pipeline triggers are "called" by the + host, so they must always have a pre-defined authority. + +Definition example: + +```rust +enum VariableAuthority { + /// Always use this account + Specified(AccountId), + /// Use the caller's authority + Caller, +} +``` + +## Consideration: triggers are a part of transaction? + +Currently, data triggers are a part of the original transaction, and if data trigger fails - the entire transaction +fails. + +This seems to be useful in some cases, but is not compatible with the case when Data trigger has a pre-defined +authority: Alice then could fail Bob's transactions if she wants. However, it may also be desirable: an admin may want +to fail someone's transaction by their data trigger. + +Here, I would propose a mixed approach too: specify whether the trigger is **attached or detached** from the original +transaction. This must come with some default-executor-permission like `CanAttachTriggersTo { AccountId }`, so that only +users with right permissions could attach their triggers, and by default, all triggers would act as an independent +transaction. + +## Security Considerations + +- Risk of privilege escalation if trigger logic is careless +- Need to treat triggers with pre-defined authority as “contracts” that must validate caller input +- Possible DoS if malicious actors span triggers with invalid inputs + +However, some of these could be mitigated by permission system. E.g. currently default executor has +`CanExecuteTrigger { TriggerId }` permissions which could be used to have "global whitelisting". + +## Conclusion + +So, I would personally prefer **Option 2** - the mixed approach. However, this requires some data model restructuring to +reflect that not every trigger can have a variable authority. + +**Option 1** also seems better for me than what it is right now, because it enables privileged automation and simplifies +reasoning. As it is simple to implement and does not interfere with future Option 2, I'd welcome targeting it +immediately for the convenience of the Hub Chain PoC.