Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
${{ runner.os }}-sbt-

- name: Run tests with coverage
run: sbt clean coverage sharedData/test coverageReport
run: sbt clean coverage proto/compile proto/test sharedData/test coverageReport

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
Expand Down
145 changes: 145 additions & 0 deletions modules/proto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# OttoChain Proto Module

This module provides Protocol Buffer definitions and ScalaPB-generated Scala types for OttoChain data structures.

## Build-Time Generation Strategy

### ScalaPB Configuration

The module uses **build-time generation** via sbt-protoc and ScalaPB:

```scala
Compile / PB.targets := Seq(
scalapb.gen(flatPackage = true) -> (Compile / sourceManaged).value / "scalapb",
scalapb.validate.gen() -> (Compile / sourceManaged).value / "scalapb"
)
```

**Generated files location:** `target/scala-2.13/src_managed/main/scalapb/` (gitignored)

**Benefits:**
- Consistent with Tessellation's build strategy
- Generated types available at compile time
- No runtime protoc dependencies
- IDE integration works seamlessly

### DataUpdate Mixin Integration

All OttoChain message types extend `io.constellationnetwork.currency.dataApplication.DataUpdate` via ScalaPB options:

```protobuf
message CreateStateMachine {
option (scalapb.message).extends = "io.constellationnetwork.currency.dataApplication.DataUpdate";
// ...
}
```

This enables:
- Direct submission to Tessellation's DataL1
- Type-safe metagraph integration
- Seamless ML0 processing

## Type System Architecture

### Proto vs. Scala Model Boundary

**Key architectural decision:** Proto types and Scala domain models serve different purposes and coexist:

- **Proto types** (`ottochain.v1.*`): Wire format, serialization, network transport
- **Scala models** (`xyz.kd5ujc.models.*`): Domain logic, business rules, type safety

### ProtoAdapters Migration Strategy

The `ProtoAdapters.scala` module handles conversion between proto and domain types:

```scala
// Phase 1: Records conversion (outbound only)
def toProto(record: FiberRecord): ottochain.v1.FiberRecord = ...

// Phase 2: Message conversion (inbound + outbound) - after PR #89
def fromProto(msg: ottochain.v1.OttochainMessage): models.Transaction = ...
```

**Current status:** Phase 1 complete, Phase 2 planned post-fiber-engine migration.

### Sequenced Trait Structural Gap

Generated proto types have structural differences from hand-written Scala types:

- **Generated:** `fiberId: String`, `targetSequenceNumber: Option[proto.FiberOrdinal]`
- **Domain models:** `fiberId: UUID`, `sequenceNumber: models.FiberOrdinal`

The `Sequenced` trait cannot be directly implemented by generated types. ProtoAdapters bridges this gap during conversion.

## Testing Strategy

### Integration Tests

`ScalaPBIntegrationTest.scala` verifies:
1. **Compilation:** Generated types compile successfully
2. **Mixin verification:** All message types extend `DataUpdate`
3. **Type safety:** Constructor and field access work correctly
4. **Union types:** `OttochainMessage` oneof functionality

**Coverage:** 6 message types tested (CreateStateMachine, TransitionStateMachine, ArchiveStateMachine, CreateScript, InvokeScript, OttochainMessage)

### CI Integration

Proto module included in CI pipeline:
```bash
sbt proto/compile proto/test
```

This ensures:
- Proto definitions compile successfully
- Generated types satisfy type constraints
- Mixin injection works correctly
- No regressions in proto → Scala conversion

## Proto Definitions

### Core Messages

| Message | Purpose | Key Fields |
|---------|---------|------------|
| `CreateStateMachine` | Create new fiber | `fiber_id`, `definition`, `initial_data` |
| `TransitionStateMachine` | Trigger state transition | `fiber_id`, `event_name`, `payload` |
| `ArchiveStateMachine` | Archive fiber | `fiber_id`, `target_sequence_number` |
| `CreateScript` | Create script fiber | `fiber_id`, `script_program`, `access_control` |
| `InvokeScript` | Execute script method | `fiber_id`, `method`, `args` |
| `OttochainMessage` | Union of all messages | `oneof message` |

### Validation Rules

Proto definitions include validation via protoc-gen-validate:
- `fiber_id`: minimum length 1
- `event_name`: minimum length 1
- `method`: minimum length 1

## Development Workflow

### Adding New Message Types

1. **Define proto message** in `src/main/protobuf/ottochain/v1/messages.proto`
2. **Add DataUpdate mixin** via `scalapb.message.extends` option
3. **Add to union** in `OttochainMessage.oneof`
4. **Write integration test** in `ScalaPBIntegrationTest.scala`
5. **Update ProtoAdapters** (if domain model exists)

### Proto Schema Evolution

**Field numbering policy:**
- Reserve ranges 1-100 for core fields
- Reserve 101-200 for extensions
- Never reuse field numbers
- Document breaking changes in migration guide

### Generated Code Inspection

Generated Scala files are in `target/scala-2.13/src_managed/main/scalapb/`.

**Useful for debugging:**
- Verify mixin inheritance
- Check constructor signatures
- Inspect validation logic
- Understand union type structure
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.effect.IO

import io.constellationnetwork.currency.dataApplication.DataUpdate

import ottochain.v1.{CreateStateMachine, TransitionStateMachine}
import ottochain.v1._
import weaver.SimpleIOSuite

object ScalaPBIntegrationTest extends SimpleIOSuite {
Expand Down Expand Up @@ -43,4 +43,80 @@ object ScalaPBIntegrationTest extends SimpleIOSuite {
expect(dataUpdate.isInstanceOf[DataUpdate])
}
}

test("Generated ArchiveStateMachine extends DataUpdate") {
IO {
val archiveSM = ArchiveStateMachine(
fiberId = "test-archive-id",
targetSequenceNumber = Some(FiberOrdinal(value = 42))
)

// This should compile successfully if DataUpdate mixin works
val dataUpdate: DataUpdate = archiveSM

expect(archiveSM.fiberId == "test-archive-id") &&
expect(archiveSM.targetSequenceNumber.exists(_.value == 42)) &&
expect(dataUpdate.isInstanceOf[DataUpdate])
}
}

test("Generated CreateScript extends DataUpdate") {
IO {
val createScript = CreateScript(
fiberId = "test-script-id",
scriptProgram = None,
initialState = None,
accessControl = Some(AccessControlPolicy())
)

// This should compile successfully if DataUpdate mixin works
val dataUpdate: DataUpdate = createScript

expect(createScript.fiberId == "test-script-id") &&
expect(createScript.accessControl.isDefined) &&
expect(dataUpdate.isInstanceOf[DataUpdate])
}
}

test("Generated InvokeScript extends DataUpdate") {
IO {
val invokeScript = InvokeScript(
fiberId = "test-invoke-id",
method = "execute",
args = None,
targetSequenceNumber = Some(FiberOrdinal(value = 10))
)

// This should compile successfully if DataUpdate mixin works
val dataUpdate: DataUpdate = invokeScript

expect(invokeScript.fiberId == "test-invoke-id") &&
expect(invokeScript.method == "execute") &&
expect(invokeScript.targetSequenceNumber.exists(_.value == 10)) &&
expect(dataUpdate.isInstanceOf[DataUpdate])
}
}

test("Generated OttochainMessage union extends DataUpdate") {
IO {
// Test with CreateStateMachine variant
val createSM = CreateStateMachine(
fiberId = "union-test-id",
definition = None,
initialData = None,
parentFiberId = None
)

val ottochainMessage = OttochainMessage(
message = OttochainMessage.Message.CreateStateMachine(createSM)
)

// This should compile successfully if DataUpdate mixin works
val dataUpdate: DataUpdate = ottochainMessage

expect(ottochainMessage.message.isCreateStateMachine) &&
expect(ottochainMessage.message.createStateMachine.exists(_.fiberId == "union-test-id")) &&
expect(dataUpdate.isInstanceOf[DataUpdate])
}
}
}
Loading