Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions text/003-trigger-authority.md
Original file line number Diff line number Diff line change
@@ -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. | |
Comment on lines +21 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my view, the only clearly useful programmable execution systems currently identified are the following three:

  • smart contract (integrated with by-call trigger and custom instruction)
  • data trigger
  • time trigger

I am skeptical about providing other types of triggers. For details, see hyperledger-iroha/iroha#5512.


## 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.
Comment on lines +61 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative, Alice could keep a multisig proposal pending and have it approved by a data trigger, which would achieve the same effect.

Copy link
Contributor Author

@0x009922 0x009922 Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it sound kind of complicated?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it may sound more complex — but that complexity allows us to avoid introducing fixed authorities and implicit privilege escalation. There is a clear trade-off here:

  • On one side, fixed-authority triggers look simple, but they open the door to confused-deputy problems and unintended elevation.
  • On the other, approaches like multisig, time triggers, or other two-phase commit flows are somewhat more complex, but they keep the execution model consistent and auditable, with no hidden authority switches.

In a security-critical system like Iroha, it is better to accept a bit more design complexity than to adopt a mechanism that weakens the permission model.


## 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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it specified in the trigger definition at the time of creation, or is it set when the trigger is executed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as now - defined at the time of registration, once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only things that are mutable in triggers (after registration) are their:

  1. Metadata
  2. Remaining repetition amount

- 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.
Comment on lines +104 to +111
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The control of whether someone’s transaction succeeds or fails should be centralized in the permission system.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Permission system allows for fine-grained access to resources and actions. I am not sure I understand why it should be centralized.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key issue is that the entity subscribing to an event is not necessarily the legitimate administrator.
Any third party who has permission to listen to even a single event along the execution path can force the entire transaction to fail.
And of course, such a malicious actor would never register their sabotage trigger in a detached mode — they would configure it specifically so that it always propagates failure.

Copy link
Contributor Author

@0x009922 0x009922 Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And of course, such a malicious actor would never register their sabotage trigger in a detached mode — they would configure it specifically so that it always propagates failure.

Yes, this makes total sense to me. Of course, if we introduce "attached/detached" modes, it would be essential to make attaching a trigger also guarded by a permission.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historically, the permission system only governed state read/write access.
Once we bring attach/detach into scope, the permission model must also cover a second dimension: transaction scope control (propagation reach and generational boundaries):

  • Dimension 1: State Access (read/write capabilities)
  • Dimension 2: Scope Control (changing the propagation surface via attach/detach)


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
Comment on lines +118 to +122
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, fixing the authority of a data trigger means forcing a complete simulation of the event source at registration time, which can sometimes be difficult and, in my view, opens the door to unfortunate accidents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forcing a complete simulation of the event source at registration time

Could you elaborate, please?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing the authority of a data trigger effectively means that causing the trigger event is equivalent to acquiring that authority. As a result, the trigger author must design the subscription with extreme care to ensure that no unintended path can fire it.


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.