Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CIP-0112? | Observe script type #749

Merged
merged 22 commits into from
May 14, 2024
Merged
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
201 changes: 201 additions & 0 deletions cip-observe-script-type/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
---
CIP: 112
Title: Observe Script Type
Status: Proposed
Category: Plutus
Authors:
- Philip DiSarro <info@anastasialabs.com>
Implementors: []
Discussions:
- https://github.com/cardano-foundation/CIPs/pull/418/
Created: 2024-01-08
License: CC-BY-4.0
---

## Abstract
We propose to introduce a new Plutus scripts type `Observe` in addition to those currently available (spending, certifying, rewarding, minting, drep). The purpose of this script type is to allow arbitrary validation logic to be decoupled from any ledger action.
Since observe validators are decoupled from actions, you can run them in a transaction without needing to perform any associated action (ie you don't need to consume a script input, or mint a token, or withdraw from a staking script just to execute this validator).
Additionally, we propose to introduce a new assertion to native scripts that they can use to check that a particular script hash is in `required_observers` (which in turn enforces that the script must be executed successfully in the transaction). This addresses a number of technical issues discussed in other CIPs and CPS such as the redundant execution of spending scripts, and the inability to effectively use native scripts in conjunction with Plutus scripts.
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're making a change to the native script version, we should probably also get things on POSIX time rather than slot times.

Copy link
Contributor

Choose a reason for hiding this comment

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

Whether that's another CIP or part of this one, i'd probably lean to a separate CIP, but it'd be a shame to see one without the other.


## Motivation: why is this CIP necessary?
Often in a plutus validator you want to check "a particular (different) Plutus script checked this transaction", but it's annoying (and wasteful) to have to have to lock an output in a script and then check if that output is consumed, or mint a token, or whatever else just to trigger script validation.

Currently the main design pattern used to achieve this is a very obscure trick involving staking validators and the fact that you can withdraw 0 from a staking validator to trigger the script validation. A summary of the trick is:
Implement all the intended validation logic in a Plutus staking validator, we will call this validator `s_v`. To check that this validator was executed in the transaction you check if the credential of `s_v` (`StakingCredential`) is present in `txInfoWdrl`, this guarantees that `s_v` was checked in validation.
This relies on the fact that unlike in `txInfoMint` the ledger does not filter out 0 amount entries in `txInfoWdrl`. This means that you are allowed to build transactions that withdraw zero from a staking credential which in-turn triggers the staking script associated with that credential to execute in the transaction,
which makes it available in `txInfoWdrl`. This is a enables a very efficient design pattern for checking logic that is shared across multiple scripts.

For instance, a common design pattern is a token based forwarding validator in which the validator defers its logic to another validator by checking that a state token is present in one of the transaction inputs:
```haskell
forwardNFTValidator :: AssetClass -> BuiltinData -> BuiltinData -> ScriptContext -> ()
forwardNFTValidator stateToken _ _ ctx = assetClassValueOf stateToken (valueSpent (txInfo ctx)) == 1
```
This pattern is common in protocols that use the batcher architecture. Some protocols improve on the pattern by including the index of the input with the state token in the redeemer:
```haskell
forwardNFTValidator :: AssetClass -> BuiltinData -> Integer -> ScriptContext -> ()
forwardNFTValidator stateToken _ tkIdx ctx = assetClassValueOf stateToken (txInInfoResolved (elemAt tkIdx (txInfoInputs (txInfo ctx)))) == 1

forwardMintPolicy:: AssetClass -> Integer -> ScriptContext -> ()
forwardMintPolicy stateToken tkIdx ctx = assetClassValueOf stateToken (txInInfoResolved (elemAt tkIdx (txInfoInputs (txInfo ctx)))) == 1
```
The time complexity of this validator is **O(n)** where n is the number of tx inputs. This logic is executed once per input being unlocked / currency symbol being minted.
The redundant execution of searching the inputs for a token is the largest throughput bottleneck for these DApps; it is **O(n*m)** where n is the number of inputs and m is the number of `forwardValidator` inputs + `forwardValidator` minting policies.
Using the stake validator trick, the time complexity of the forwarding logic is improved to **O(1)**. The forwardValidator logic becomes:
```haskell
forwardWithStakeTrick:: StakingCredential -> BuiltinData -> BuiltinData -> ScriptContext -> ()
forwardWithStakeTrick obsScriptCred tkIdx ctx = fst (head stakeCertPairs) == obsScriptCred
where
info = txInfo ctx
stakeCertPairs = AssocMap.toList (txInfoWdrl info)

stakeValidatorWithSharedLogic :: AssetClass -> BuiltinData -> ScriptContext -> ()
stakeValidatorWithSharedLogic stateToken _rdmr ctx = assetClassValueOf stateToken (valueSpent (txInfo ctx)) == 1
```
For the staking validator trick (demonstrated above), we are simply checking that the StakingCredential of the the staking validator containing the shared validation logic is in the first pair in `txInfoWdrl`. If the StakingCredential is present in `txInfoWdrl`, that means the staking validator (with our shared validation logic) successfully executed in the transaction. This script is **O(1)** in the case where you limit it to one shared logic validator (staking validator), or if you don't want to break composability with other staking validator,
then it becomes **O(obs_N)** where `obs_N` is the number of Observe validators that are executed in the transaction as you have to verify that the StakingCredential is present in `txInfoWdrl`.

The proposed changes in this CIP enable this design pattern to exist indepedently from implementation details of stake validators and withdrawals, and improve efficiency and readability for validators that implement it. Furthermore, with the proposed extension to native scripts, we are able to completely get rid of the redundant spending script executions like so:
```haskell
observationValidator :: AssetClass -> BuiltinData -> ScriptContext -> ()
observationValidator stateToken _redeemer ctx = assetClassValueOf stateToken (valueSpent (txInfo ctx)) == 1
```
We simply include the script hash of the above `observationValidator` in the `required_observers` field in the transaction body and we lock
all the UTxOs that we would like to share the same spending condition into the following native script:
```json
{
"type": "observer",
"keyHash": "OUR_OBSERVATION_SCRIPT_HASH"
}
```
The above solution (enabled by this CIP) is more clear, concise, flexible and efficient than the alternatives discussed above.

## Specification
The type signature of this script type will be consistent with the type signature of minting and staking validators, namely:
```haskell
Redeemer -> ScriptContext -> ()
```

The type signature of the newly introduced `Purpose` will be:
```haskell
Observe Integer -- ^ where integer is the index into the observations list.
```
Comment on lines +78 to +81

Choose a reason for hiding this comment

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

Is there a reason why the purpose should include the index instead of the script hash like with minting policies and staking scripts? The spending purpose is the only one that does not provide the hash directly and I only ever use it to lookup the script's hash from the input being spent. It seems other developers do the same since there is now even this CIP which suggests changing the spending purpose to include the script's hash. I would much rather have the Purpose be:

Observe ScriptHash -- ^ the hash of the script currently observing.

Copy link
Contributor Author

@colll78 colll78 Jan 27, 2024

Choose a reason for hiding this comment

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

I would argue it should be the other way around, the spending purpose should include the index of the UTxO being spent. The standard usage for the purpose in spending scripts is to find the input that is currently being validated against.

Choose a reason for hiding this comment

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

the spending purpose should include the index of the UTxO being spent.

I think this makes sense for spending scripts but not for observation scripts. A TxInInfo contains a lot more information than just a TxOutRef or a ScriptHash. It probably doesn't make sense to put all of that information into the Purpose so an index place-holder makes sense. But for observation scripts, the only thing you can get from the TxInfoObservations is a ScriptHash, and a ScriptHash fits no problem in the Purpose. If the observation index serves no purpose other than to get the script hash for the current observation script, then doesn't it makes more sense to inline the script hash instead of using an index place-holder? Otherwise, the index is forcing the unnecessary extra step of looking up the script hash in the observation list. Do you personally have another use for the observation index in mind?

Copy link
Contributor Author

@colll78 colll78 Jan 28, 2024

Choose a reason for hiding this comment

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

Yes, I agree.

Spending Script purpose should be Spending Integer ScriptHash and observe purpose should be Observe ScriptHash.

My original idea of the index in the observe purpose was for other scripts to be able to obtain the index of the observation they are looking for from the txInfoRedeemers but this requires the same amount of work as just searching the txInfoObservations directly

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a weak spot in the current design of things to be sure. The thing that would be closest to the ledger would be to always provide a "pointer" to the thing that caused the run, in which case we should indeed probably change the Spending purpose to have an index instead. The current situation is a bit inconsistent; in some cases we resolve some of the information for you, in others not.

Copy link
Contributor Author

@colll78 colll78 Jan 29, 2024

Choose a reason for hiding this comment

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

If we do decide to resolve information for ScriptPurpose, then we should make the information we resolve consistent across all the different constructors. I think ScriptHash is a good option in that case, since it is universally useful across the common script types (Minting, Spending, Staking).

But I really like the idea of integer indices for everything because then we can move towards a future where we can better take advantage of the unique efficiency benefits offered to us by transaction determinism that other smart contract platforms (ie EVM) cannot have access to (especially if in the far future we have support for constant access indexed data structures directly in the script context). I feel like currently, we haven't even begun to scratch the surface of utilizing the advantages afforded to us via Cardano's transaction determinism.

For instance, since we know at the time of transaction construction what the majority of the script context will look like, we can theoretically bring most lookups from O(n) to O(1). Especially with TxOut Redeemers you can imagine a world where common lookup operations like findOwnInput, valueOf, findStateThreadOutput, are all O(1). Instead of looking up elements on-chain, we look them up off-chain and then we pass the index into the on-chain computation which then only has to check that the element at that index is indeed what we claim it is.


### Script context

Scripts are passed information about transactions via the script context.
We propose to augment the script context to include information about the observation scripts that are executed in the transaction.

Changing the script context will require a new Plutus language version in the ledger to support the new interface.
The change is: a new field is added to the script context which represents the list of observers that must be present in the transaction.

The interface for old versions of the language will not be changed.
Scripts with old versions cannot be spent in transactions that include observation scripts, attempting to do so will be a phase 1 transaction validation failure.

A new field will be introduced into the script context:

```haskell
-- | TxInfo for PlutusV3
data TxInfo = TxInfo
{ txInfoInputs :: [V2.TxInInfo]
, txInfoReferenceInputs :: [V2.TxInInfo]
, txInfoOutputs :: [V2.TxOut]
, txInfoFee :: V2.Value
, txInfoMint :: V2.Value
, txInfoTxCerts :: [TxCert]
, txInfoWdrl :: Map V2.Credential Haskell.Integer
, txInfoValidRange :: V2.POSIXTimeRange
, txInfoSignatories :: [V2.PubKeyHash]
, txInfoRedeemers :: Map ScriptPurpose V2.Redeemer
, txInfoData :: Map V2.DatumHash V2.Datum
, txInfoId :: V2.TxId
, txInfoVotingProcedures :: Map Voter (Map GovernanceActionId VotingProcedure)
, txInfoProposalProcedures :: [ProposalProcedure]
, txInfoCurrentTreasuryAmount :: Haskell.Maybe V2.Value
, txInfoTreasuryDonation :: Haskell.Maybe V2.Value
, txInfoObservations :: [V2.Credential] -- ^ newly introduced list of observation scripts that executed in this tx.
}
```

### CDDL

The CDDL for transaction body will change as follows to reflect the new field.
```
transaction_body =
{ 0 : set<transaction_input> ; inputs
, 1 : [* transaction_output]
, 2 : coin ; fee
, ? 3 : uint ; time to live
, ? 4 : certificates
, ? 5 : withdrawals
, ? 7 : auxiliary_data_hash
, ? 8 : uint ; validity interval start
, ? 9 : mint
, ? 11 : script_data_hash
, ? 13 : nonempty_set<transaction_input> ; collateral inputs
, ? 14 : required_observers ; Upgraded `required_signers`
, ? 15 : network_id
, ? 16 : transaction_output ; collateral return
, ? 17 : coin ; total collateral
, ? 18 : nonempty_set<transaction_input> ; reference inputs
, ? 19 : voting_procedures ; Voting procedures
, ? 20 : proposal_procedures ; Proposal procedures
, ? 21 : coin ; current treasury value
, ? 22 : positive_coin ; donation
}
; addr_keyhash variant is included for backwards compatibility and will be
; deprecated in the future era, because `credential` already contains `addr_keyhash`.
required_observers = nonempty_set<credential / addr_keyhash>
```

We rename the `required_signers` field to `required_observers`, promoting it from a list of public key hashes to a list of credentials (i.e. either a KeyHash or ScriptHash). This is consistent with other parts of the transaction that are unlocked by a script or a key witness. `required_observers` (field 14) is a set of credentials that must be satisfied by the transaction. For public key credentials, if the corresponding signature is not in the witness set, the transaction will fail in phase 1. For script credentials, if the associated scripts is not present in the witness set or as a reference script and executed in the transaction, the transaction will fail in phase 1 validation. This way Plutus scripts can check the script context to know which observation scripts were executed in the transaction. Similarly, since native script conditions use the `required_observers` field, it is natural that they are now able to require that other scripts observed the transaction (an extension of the ability to check for the presence of key signatures).

### Native Script Extension

The BNF notation for the abstract syntax of native scripts change as follows to reflect the new field.

```BNF
<native_script> ::=
<RequireSignature> <vkeyhash>
| <RequireObserver> <scripthash>
| <RequireTimeBefore> <slotno>
| <RequireTimeAfter> <slotno>

| <RequireAllOf> <native_script>*
| <RequireAnyOf> <native_script>*
| <RequireMOf> <num> <native_script>*
```

Native scripts are typically represented in JSON syntax. We propose the following JSON representation for the `RequireObserver` constructor:
```JSON
{
"type": "observer",
"keyHash": "OBSERVATION_SCRIPT_HASH"
}
```


## Rationale: how does this CIP achieve its goals?

Currently Plutus scripts (and native scripts) in a transaction will only execute when the transaction performs the associated ledger action (ie. a Plutus minting policy will only execute if the transaction mints or burns tokens with matching currency symbol). The only exception is the withdraw zero trick which relies on an obscure mechanic where zero amount withdrawals are not filtered by the ledger. Now using `required_observers` we can specify a list of scripts (supports both native and Plutus scripts) to be executed in the transaction independent of any ledger actions. The newly introduced `txInfoObservations` field in the script context provides a straightforward way for scripts to check that "a particular script validated this transaction".

This change is not backwards-compatible and will need to go into a new Plutus language version.
Copy link

@fallen-icarus fallen-icarus Feb 1, 2024

Choose a reason for hiding this comment

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

I'm not sure what the equivalent of a plutus language version is for native scripts but I am guessing they will also need a new version.

Edit: See top-level review comment.


### Alternatives

- We could decide to accept the withdraw-zero staking script trick as an adequate solution, and just preserve the nonsensical withdraw zero case in future language versions.
- The staking script trick could be abstracted away from the developer by smart contract languages that compile to UPLC.
- This can be dangerous since by distancing the developer from what is actually happening you open up the door for the developer to act on misguided assumptions.

## Path to Active

### Acceptance Criteria
- [ ] These rules included within a official Plutus version, and released via a major hard fork.

### Implementation Plan
- [ ] Passes all requirements of both Plutus and Ledger teams as agreed to improve Plutus script efficiency and usability.

## Copyright
This CIP is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode).

colll78 marked this conversation as resolved.
Show resolved Hide resolved
[CC-BY-4.0]: https://creativecommons.org/licenses/by/4.0/legalcode
[Apache-2.0]: http://www.apache.org/licenses/LICENSE-2.0