|
| 1 | +# EventTriggerDefinition Format Specification |
| 2 | + |
| 3 | +This specification provides the format and encoding rules necessary to implement |
| 4 | +tooling that generates `EventTriggerDefinition` instances programmatically. |
| 5 | +These |
| 6 | +[`triggerDefinition`](https://github.com/shutter-network/contracts/blob/8b0051b13875a7450811c4634e72f2edad7f5016/src/shutter-service/ShutterEventTriggerRegistry.sol#L38)s |
| 7 | +are used to register event based decryption triggers. Keypers that take part in |
| 8 | +a keyper set that has these enabled, will watch the blockchain for events |
| 9 | +matching these definitions. The registration is done through a contract specific |
| 10 | +to the keyperset. An example |
| 11 | +[can be seen here](https://github.com/shutter-network/contracts/blob/8b0051b13875a7450811c4634e72f2edad7f5016/src/shutter-service/ShutterEventTriggerRegistry.sol). |
| 12 | + |
| 13 | +## Elements / Types |
| 14 | + |
| 15 | +### EventTriggerDefinition |
| 16 | + |
| 17 | +An `EventTriggerDefinition` is a structured data object that combines: |
| 18 | + |
| 19 | +- A reference to a specific smart contract (by address) |
| 20 | +- A set of log predicates that filter which events from that contract should |
| 21 | + trigger the release of the decryption key. |
| 22 | + |
| 23 | +Reference: |
| 24 | +[EventTriggerDefinition Type](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L21-L25) |
| 25 | + |
| 26 | +### LogPredicate |
| 27 | + |
| 28 | +A `LogPredicate` pairs a reference to a specific value within an event log with |
| 29 | +a predicate that must be satisfied for the trigger to fire. |
| 30 | + |
| 31 | +Reference: |
| 32 | +[LogPredicate Type](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L27-L32) |
| 33 | + |
| 34 | +### LogValueRef |
| 35 | + |
| 36 | +A `LogValueRef` identifies which value within an event log should be evaluated. |
| 37 | +It can reference: |
| 38 | + |
| 39 | +- **Topics (Offsets 0-3)**: The indexed parameters of the event log. A topic is |
| 40 | + referenced by its index (0-3), where offset 0 is topic[0], etc. |
| 41 | +- **Data (Offsets 4+)**: The non-indexed data section of the log. Offset |
| 42 | + values >= 4 refer to 32-byte words in the log data, where offset 4 is the |
| 43 | + first word (bytes 0-31), offset 5 is the second word (bytes 32-63), etc. Note: |
| 44 | + data offset always starts at 4, even if there are fewer than 4 topics defined. |
| 45 | + |
| 46 | +Reference: |
| 47 | +[LogValueRef Type and Documentation](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L34-L43) |
| 48 | + |
| 49 | +### ValuePredicate |
| 50 | + |
| 51 | +A `ValuePredicate` defines the condition that must be satisfied on a referenced |
| 52 | +log value. It consists of an operation (`Op`) and a set of arguments. The type |
| 53 | +and number of arguments required depend on the operation. |
| 54 | + |
| 55 | +Reference: |
| 56 | +[ValuePredicate Type](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L45-L53) |
| 57 | + |
| 58 | +## Encoding Format |
| 59 | + |
| 60 | +### Binary Encoding |
| 61 | + |
| 62 | +`EventTriggerDefinition` values are encoded using RLP (Recursive Length Prefix) |
| 63 | +encoding with a version byte prefix: |
| 64 | + |
| 65 | +``` |
| 66 | +EventTriggerDefinitionBytes := { |
| 67 | + version: uint8 // Current version is 0x01 |
| 68 | + rlp_encoded_content: []byte // RLP-encoded EventTriggerDefinition |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +The version byte (`0x01`) allows for future format changes. After the version |
| 73 | +byte, the remaining bytes are RLP-encoded content representing the |
| 74 | +`EventTriggerDefinition` structure. |
| 75 | + |
| 76 | +Reference: |
| 77 | +[MarshalBytes and UnmarshalBytes Implementation](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L58-L86) |
| 78 | + |
| 79 | +### Versioning |
| 80 | + |
| 81 | +The current version is **0x01** as defined by the `Version` constant. |
| 82 | + |
| 83 | +### RLP Encoding Details |
| 84 | + |
| 85 | +The RLP encoding format for the core structures are just nested RLP lists. The |
| 86 | +named object definitions below are for clarification purposes only: |
| 87 | + |
| 88 | +``` |
| 89 | +EventTriggerDefinition := { |
| 90 | + contract: Address, |
| 91 | + logPredicates: [LogPredicate, ...] |
| 92 | +} |
| 93 | +
|
| 94 | +LogPredicate := { |
| 95 | + logValueRef: LogValueRef, |
| 96 | + valuePredicate: ValuePredicate |
| 97 | +} |
| 98 | +
|
| 99 | +LogValueRef := Offset // if Length == 1 |
| 100 | +LogValueRef := [Offset, Length] // if Length > 1 |
| 101 | +
|
| 102 | +ValuePredicate := { |
| 103 | + op: uint64, |
| 104 | + intArgs: [BigInt, ...], |
| 105 | + byteArgs: [Bytes, ...] |
| 106 | +} |
| 107 | +
|
| 108 | +# actual dense format is more akin to this: |
| 109 | +[contract_address, [[offset, [op, [int_args], [byte_args]]], […]]] |
| 110 | +``` |
| 111 | + |
| 112 | +Reference: |
| 113 | +[RLP Encoding Implementation](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L211-L392) |
| 114 | + |
| 115 | +## Operators |
| 116 | + |
| 117 | +The `Op` type defines the operations that can be performed when evaluating a |
| 118 | +value predicate. |
| 119 | + |
| 120 | +### Supported Operators |
| 121 | + |
| 122 | +- **`Uint...` (0-4) Operators**: Unsigned integer (`uint256` / `big.Int`) |
| 123 | + comparisons |
| 124 | + - Argument: 1 unsigned integer |
| 125 | + - **`UintLt` (0)** |
| 126 | + - Returns true if `value < argument` |
| 127 | + - **`UintLte` (1)** |
| 128 | + - Returns true if `value <= argument` |
| 129 | + - **`UintEq` (2)** |
| 130 | + - Returns true if `value == argument` |
| 131 | + - **`UintGt` (3)** |
| 132 | + - Returns true if `value > argument` |
| 133 | + - **`UintGte` (4)** |
| 134 | + - Returns true if `value >= argument` |
| 135 | +- **`BytesEq` (5)**: Byte sequence equality comparison |
| 136 | + - Arguments: 1 byte sequence (exactly matching the value size) |
| 137 | + - Returns true if value == argument (byte-by-byte comparison) |
| 138 | + |
| 139 | +Reference: |
| 140 | +[Operator Constants](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L298-L305) |
| 141 | + |
| 142 | +Note, that different operators require different numbers and types of arguments: |
| 143 | + |
| 144 | +- **Uint...-operators**: 1 integer argument, 0 byte arguments |
| 145 | +- **BytesEq**: 0 integer arguments, 1 byte argument |
| 146 | + |
| 147 | +## Matching Logs Against Definitions |
| 148 | + |
| 149 | +In the keyper implementation, the `Match` method is used to check if a given |
| 150 | +Ethereum log satisfies the `EventTriggerDefinition`: |
| 151 | + |
| 152 | +1. The log's contract address must match the definition's contract address |
| 153 | +2. The log must satisfy **all** log predicates (logical `AND` of all conditions) |
| 154 | + |
| 155 | +Reference: |
| 156 | +[Match Method](https://github.com/shutter-network/rolling-shutter/blob/42f562532acfc4f89f630d3de809fc4451636ab2/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L159-L176) |
| 157 | + |
| 158 | +### LogValueRef.GetValue |
| 159 | + |
| 160 | +To retrieve a value from a log: |
| 161 | + |
| 162 | +- **Topics (Offset < 4)**: Returns the log's topic at index Offset as a 32-byte |
| 163 | + value |
| 164 | +- **Data (Offset >= 4)**: Extracts a slice of 32-byte words from the log's data, |
| 165 | + starting at word index (Offset - 4). If the slice extends beyond the log's |
| 166 | + data, it is zero-padded on the right to the expected length. |
| 167 | + |
| 168 | +Reference: |
| 169 | +[GetValue Method](https://github.com/shutter-network/rolling-shutter/blob/main/rolling-shutter/keyperimpl/shutterservice/eventtrigger.go#L210-L228) |
| 170 | + |
| 171 | +## Generating EventTriggerDefinition from EVM Contract ABI |
| 172 | + |
| 173 | +To generate an `EventTriggerDefinition` from an EVM contract ABI and a set of |
| 174 | +predicates: |
| 175 | + |
| 176 | +### Input Requirements |
| 177 | + |
| 178 | +1. **Contract Address**: The Ethereum address where the contract is deployed |
| 179 | +2. **Contract ABI**: Standard Solidity JSON ABI format |
| 180 | +3. **Target Event Name**: The name of the event to trigger on |
| 181 | +4. **Predicate Specifications**: List of conditions, each specifying: |
| 182 | + - Parameter name (e.g., "to", "amount") or parameter index |
| 183 | + - Whether it's indexed or not (helps determine offset calculation) |
| 184 | + - Operation to apply (UintLt, UintEq, BytesEq, etc.) |
| 185 | + - Argument value(s) |
| 186 | + |
| 187 | +### Algorithm Overview |
| 188 | + |
| 189 | +``` |
| 190 | +function generateEventTriggerDefinition( |
| 191 | + contractAddress: Address, |
| 192 | + abi: ContractABI, |
| 193 | + eventName: string, |
| 194 | + predicateSpecs: PredicateSpec[] |
| 195 | +): EventTriggerDefinition { |
| 196 | +
|
| 197 | + // 1. Find the event in the ABI |
| 198 | + event = findEventInABI(abi, eventName) |
| 199 | + if (!event) { |
| 200 | + throw EventNotFoundError(eventName) |
| 201 | + } |
| 202 | +
|
| 203 | + // 2. Build log predicates from predicate specifications |
| 204 | + logPredicates = [] |
| 205 | +
|
| 206 | + for spec in predicateSpecs { |
| 207 | + // Find the parameter in the event |
| 208 | + param = findParameterInEvent(event, spec.parameterName) |
| 209 | + if (!param) { |
| 210 | + throw ParameterNotFoundError(spec.parameterName) |
| 211 | + } |
| 212 | +
|
| 213 | + // Calculate the LogValueRef offset |
| 214 | + if (param.indexed) { |
| 215 | + // Topics are at offsets 0-3 |
| 216 | + offset = calculateTopicIndex(event, param) |
| 217 | + length = 1 |
| 218 | + } else { |
| 219 | + // Data starts at offset 4 |
| 220 | + offset = 4 + calculateDataWordOffset(event, param) |
| 221 | + length = calculateDataWordLength(param.type) |
| 222 | + } |
| 223 | +
|
| 224 | + logValueRef = LogValueRef{ |
| 225 | + Offset: offset, |
| 226 | + Length: length |
| 227 | + } |
| 228 | +
|
| 229 | + // Create the ValuePredicate |
| 230 | + valuePredicate = createValuePredicate( |
| 231 | + spec.operation, |
| 232 | + spec.value, |
| 233 | + param.type |
| 234 | + ) |
| 235 | +
|
| 236 | + logPredicates.append(LogPredicate{ |
| 237 | + LogValueRef: logValueRef, |
| 238 | + ValuePredicate: valuePredicate |
| 239 | + }) |
| 240 | + } |
| 241 | +
|
| 242 | + // 3. Assemble and return |
| 243 | + return EventTriggerDefinition{ |
| 244 | + Contract: contractAddress, |
| 245 | + LogPredicates: logPredicates |
| 246 | + } |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +### Calculating Offsets |
| 251 | + |
| 252 | +**For Indexed Parameters (Topics)**: |
| 253 | + |
| 254 | +Topics are indexed parameters in Solidity events and are filtered by topic |
| 255 | +values in the EVM log. The offset corresponds to the topic index: |
| 256 | + |
| 257 | +- Topic 0 is always the event signature hash and is implicit (offset 0 if |
| 258 | + included) |
| 259 | +- Additional topics (offset 1, 2, 3) correspond to indexed parameters |
| 260 | + |
| 261 | +**For Non-Indexed Parameters (Data)**: |
| 262 | + |
| 263 | +Non-indexed parameters are stored in the log's data field. They are packed as |
| 264 | +32-byte words: |
| 265 | + |
| 266 | +- Word 0 (bytes 0-31) corresponds to offset 4 |
| 267 | +- Word 1 (bytes 32-63) corresponds to offset 5 |
| 268 | +- And so on... |
| 269 | + |
| 270 | +For types smaller than 32 bytes, they are right-padded (for fixed-size types) or |
| 271 | +stored in a single word. |
| 272 | + |
| 273 | +## Examples |
| 274 | + |
| 275 | +### Example 1: ERC20 Transfer Trigger |
| 276 | + |
| 277 | +Trigger when a specific address transfers tokens: |
| 278 | + |
| 279 | +**Event ABI:** |
| 280 | + |
| 281 | +```json |
| 282 | +{ |
| 283 | + "name": "Transfer", |
| 284 | + "inputs": [ |
| 285 | + { "name": "from", "type": "address", "indexed": true }, |
| 286 | + { "name": "to", "type": "address", "indexed": true }, |
| 287 | + { "name": "value", "type": "uint256", "indexed": false } |
| 288 | + ] |
| 289 | +} |
| 290 | +``` |
| 291 | + |
| 292 | +**Predicate Specification:** |
| 293 | + |
| 294 | +- Parameter "from" equals 0x742d35Cc6634C0532925a3b844Bc9e7595f1bEb |
| 295 | + |
| 296 | +**Generated EventTriggerDefinition (JSON):** |
| 297 | + |
| 298 | +```json |
| 299 | +{ |
| 300 | + "version": 1, |
| 301 | + "contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", |
| 302 | + "logPredicates": [ |
| 303 | + { |
| 304 | + "logValueRef": { |
| 305 | + "offset": 1, |
| 306 | + "length": 1 |
| 307 | + }, |
| 308 | + "valuePredicate": { |
| 309 | + "op": 5, |
| 310 | + "intArgs": [], |
| 311 | + "byteArgs": [ |
| 312 | + "0x000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f1beb" |
| 313 | + ] |
| 314 | + } |
| 315 | + } |
| 316 | + ] |
| 317 | +} |
| 318 | +``` |
| 319 | + |
| 320 | +### Example 2: Multi-Condition Trigger |
| 321 | + |
| 322 | +Trigger when a swap occurs with minimum token amount: |
| 323 | + |
| 324 | +**Event ABI (Uniswap V3 SwapRouter):** |
| 325 | + |
| 326 | +```json |
| 327 | +{ |
| 328 | + "name": "Swap", |
| 329 | + "inputs": [ |
| 330 | + { "name": "sender", "type": "address", "indexed": true }, |
| 331 | + { "name": "recipient", "type": "address", "indexed": true }, |
| 332 | + { "name": "amount0Delta", "type": "int256", "indexed": false }, |
| 333 | + { "name": "amount1Delta", "type": "int256", "indexed": false } |
| 334 | + ] |
| 335 | +} |
| 336 | +``` |
| 337 | + |
| 338 | +**Predicate Specifications:** |
| 339 | + |
| 340 | +- Parameter "amount1Delta" >= 1000000000000000000 (1 token with 18 decimals) |
| 341 | +- Parameter "sender" equals 0x742d35Cc6634C0532925a3b844Bc9e7595f1bEb |
| 342 | + |
| 343 | +**Generated EventTriggerDefinition (JSON):** |
| 344 | + |
| 345 | +```json |
| 346 | +{ |
| 347 | + "version": 1, |
| 348 | + "contract": "0x1111111254fb6d44bac0bed2854e76f90643097d", |
| 349 | + "logPredicates": [ |
| 350 | + { |
| 351 | + "logValueRef": { |
| 352 | + "offset": 1, |
| 353 | + "length": 1 |
| 354 | + }, |
| 355 | + "valuePredicate": { |
| 356 | + "op": 5, |
| 357 | + "intArgs": [], |
| 358 | + "byteArgs": [ |
| 359 | + "0x000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f1beb" |
| 360 | + ] |
| 361 | + } |
| 362 | + }, |
| 363 | + { |
| 364 | + "logValueRef": { |
| 365 | + "offset": 5, |
| 366 | + "length": 1 |
| 367 | + }, |
| 368 | + "valuePredicate": { |
| 369 | + "op": 4, |
| 370 | + "intArgs": ["1000000000000000000"], |
| 371 | + "byteArgs": [] |
| 372 | + } |
| 373 | + } |
| 374 | + ] |
| 375 | +} |
| 376 | +``` |
0 commit comments