Skip to content
Merged
Show file tree
Hide file tree
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
91 changes: 67 additions & 24 deletions docs/DECISIONS/2026-05-12-flowpulse-observation-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Date: 2026-05-12

## Status

Accepted for the MVP foundation.
Accepted for the V0 local indexer/verifier package and MVP foundation.

## Context

Expand All @@ -14,10 +14,11 @@ The indexer needs a canonical identity for an observed FlowPulse log after recei

## Decision

FlowMemory will use three separate identifiers in the indexer/verifier MVP:
FlowMemory V0 uses separate identifiers:

- `pulseId`: emitted by the contract inside the FlowPulse event.
- `observationId`: derived by the indexer from receipt/log metadata after execution.
- `cursorId`: derived by the indexer for deterministic fixture scan progress.
- `reportId`: derived by the verifier from canonical JSON report content.

`pulseId` is protocol data, not canonical observation identity. The canonical indexer identity is `observationId`.
Expand All @@ -38,53 +39,92 @@ The `topic0` hash is:

## Observation ID

The indexer derives `observationId` from:
The indexer derives `observationId` from the crypto V0 type:

```text
FlowPulseObservationV0(uint256 chainId,address emittingContract,uint64 blockNumber,bytes32 blockHash,bytes32 txHash,uint32 transactionIndex,uint32 logIndex,bytes32 eventSignature,bytes32 pulseId,bytes32 rootfieldId)
```

Fields:

- Domain: `flowmemory.flowpulse.observation.v0`
- `chainId`
- `emittingContract`
- `eventSignature`
- `blockNumber`
- `blockHash`
- `txHash`
- `transactionIndex`
- `logIndex`
- `eventSignature`
- `pulseId`
- `rootfieldId`

Preimage:

```text
keccak256(abi.encode(
"flowmemory.flowpulse.observation.v0",
keccak256("FlowPulseObservationV0(uint256 chainId,address emittingContract,uint64 blockNumber,bytes32 blockHash,bytes32 txHash,uint32 transactionIndex,uint32 logIndex,bytes32 eventSignature,bytes32 pulseId,bytes32 rootfieldId)"),
chainId,
emittingContract,
eventSignature,
blockNumber,
blockHash,
txHash,
transactionIndex,
logIndex
logIndex,
eventSignature,
pulseId,
rootfieldId
))
```

`blockHash` and `blockNumber` are included so a reorged log occurrence and a later re-mined occurrence can be represented as distinct observations. `eventSignature` is included so the identity is explicitly scoped to FlowPulse v0 logs, not arbitrary logs at the same receipt location.
`blockHash` and `blockNumber` are included so a reorged log occurrence and a later re-mined occurrence can be represented as distinct observations. `eventSignature` scopes the identity to FlowPulse v0 logs, not arbitrary logs at the same receipt location.

Decoded fields such as `actor`, `pulseType`, `subject`, `commitment`, `parentPulseId`, `sequence`, `occurredAt`, and `uri` are stored with the observation but are not part of the identity preimage. If the same `observationId` is observed with different decoded fields, that is an indexer integrity failure.

## Cursor ID

The indexer derives `cursorId` from:

- Domain: `flowmemory.indexer.cursor.v0`
- `chainId`
- `sourceSetId`
- `blockNumber`
- `blockHash`
- `transactionIndex`
- `logIndex`

Preimage:

```text
keccak256(abi.encode(
"flowmemory.indexer.cursor.v0",
chainId,
sourceSetId,
blockNumber,
blockHash,
transactionIndex,
logIndex
))
```

Decoded fields such as `pulseId`, `rootfieldId`, `actor`, `pulseType`, `subject`, `commitment`, `parentPulseId`, `sequence`, `occurredAt`, and `uri` are stored with the observation but are not part of the identity preimage. If the same `observationId` is observed with different decoded fields, that is an indexer integrity failure.
`sourceSetId` is a deterministic hash of the chain id and normalized emitting-contract set. Cursor identity includes block hash so scan progress can detect a changed canonical chain.

## Lifecycle Names

- `pending`: candidate scan work or pre-receipt/pre-finality context.
- `mined`: successful receipt contains a decodable FlowPulse log and `observationId` exists.
- `finalized`: mined observation remains canonical after the configured finality policy.
- `reorged`: observation block/log is no longer canonical or was marked removed.
- `observed`: successful receipt contains a decodable FlowPulse log and no finality policy is applied.
- `pending`: decoded observation is above the configured finality threshold.
- `finalized`: decoded observation is at or below the configured finality threshold.
- `removed`: provider marks the log removed.
- `superseded`: an older observation for the same `pulseId` was replaced.
- `reorged`: canonical block-hash check shows the indexed block is no longer canonical.

The MVP does not require mempool indexing. A canonical `observationId` begins at `mined`.
The V0 package models these states with fixtures. It does not implement production reorg handling.

## Duplicate Names

- Exact duplicate: same `observationId` and same decoded content; idempotent replay.
- Conflicting duplicate: same `observationId` but changed decoded content or metadata; indexer integrity failure.
- Pulse duplicate: same contract-emitted `pulseId` but different `observationId`; separate observations that require verifier/operator policy.
- Reorg replacement: different `observationId` caused by changed `blockHash`, block position, transaction position, or log position.
- Exact duplicate: same `observationId` and same canonical observation JSON; idempotent replay.
- Conflicting duplicate: same `observationId` but changed canonical content; indexer integrity failure.
- Pulse duplicate: same contract-emitted `pulseId` but different `observationId`; preserve both observations.
- Reorg replacement: same `pulseId` at a changed block/log location.

## Report ID

Expand All @@ -94,29 +134,32 @@ The verifier derives `reportId` from the canonical report body:
keccak256(canonical_json(reportCore))
```

The `reportCore` includes `schema = flowmemory.verifier.report.v0`, `observationId`, observed receipt/log metadata, decoded FlowPulse fields, status, reason codes, resolver policy id, and verifier spec version. The report id does not include signatures, wall-clock generation timestamps, local file paths, or operator notes.
The `reportCore` includes `schema = flowmemory.verifier.report.v0`, `observationId`, observed receipt/log metadata, decoded FlowPulse fields, status, reason codes, evidence refs, resolver policy id, and verifier spec version. The report id does not include signatures, wall-clock generation timestamps, local file paths, or operator notes.

## Consequences

- Contracts remain unaware of receipt-only metadata.
- Indexers can distinguish contract-emitted pulse identity from observed-log identity.
- Reorged observations remain addressable for audits without being treated as current canonical facts.
- Cursor progress can include block hash without changing observation identity.
- Verifiers can produce deterministic reports bound to receipt/log facts.
- Dashboards and explorers can distinguish observed, verified, unresolved, unsupported, failed, reorged, stale, disputed, and superseded outcomes later without changing the observation identity.

## Out Of Scope

- Production indexer runtime.
- Live RPC integration.
- Database schema.
- Production live RPC deployment.
- Production database schema.
- Verifier economics.
- Proof network.
- Artifact canonicalization format.
- Resolver policy.
- Resolver policy beyond local fixtures.
- Report signing or verifier attestation implementation.

## Follow-Ups

- Define artifact commitment canonicalization.
- Define resolver policy v0.
- Define resolver policy beyond local fixtures.
- Define Base finality policy.
- Define durable persistence schema.
- Decide whether future report attestations should use EIP-712 or another signature format.
Loading
Loading