canopy (Agent Skills)

GitHub

The official go implementation of the Canopy Network protocol

plugin/csharp/AGENTS.md

# AGENTS.md - C# Plugin for Canopy

This document provides guidance for AI agents working with the Canopy C# plugin codebase.

## Project Overview

This is a **C# plugin for the Canopy blockchain** that implements smart contract logic for a nested chain. The plugin communicates with the Canopy node via Unix sockets using Protocol Buffers for message serialization.

### Key Concepts

- **Plugin Architecture**: The plugin runs as a separate process and communicates with Canopy via Unix socket IPC
- **Transaction Types**: The base plugin supports "send" transactions; additional types (faucet, reward) are demonstrated in the tutorial
- **BLS12-381 Signatures**: Canopy uses BLS signatures with a custom DST (`BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_`) compatible with drand/kyber
- **Protobuf**: All messages are serialized using Protocol Buffers

## Project Structure

```
plugin/csharp/
├── src/CanopyPlugin/
│   ├── contract.cs          # Main contract logic (CheckTx, DeliverTx)
│   └── plugin.cs            # Plugin server and socket communication
├── proto/                    # Protobuf definitions
│   ├── tx.proto             # Transaction messages (MessageSend, etc.)
│   ├── plugin.proto         # Plugin protocol messages
│   ├── account.proto        # Account state
│   └── event.proto          # Event types
├── tutorial/                 # Tutorial project with extended features
│   ├── Crypto/BLSCrypto.cs  # BLS signing utilities
│   ├── RpcTest.cs           # Integration tests
│   └── CanopyPlugin.Tutorial.csproj
├── CanopyPlugin.csproj      # Main project file
├── CanopyPlugin.sln         # Solution file
├── Makefile                 # Build and test commands
├── TUTORIAL.md              # Guide for adding new transaction types
└── global.json              # .NET SDK version (8.0)
```

## Key Files

### `src/CanopyPlugin/contract.cs`
The main contract implementation containing:
- `ContractConfig` - Plugin configuration (name, ID, supported transactions, type URLs)
- `Contract` class with:
  - `CheckTxAsync()` - Stateless transaction validation
  - `DeliverTxAsync()` - Transaction execution with state changes
  - `Genesis()`, `BeginBlock()`, `EndBlock()` - Lifecycle hooks
- State key generation methods (`KeyForAccount`, `KeyForFeePool`, etc.)
- Error factory methods

### `src/CanopyPlugin/plugin.cs`
Plugin server that:
- Listens on Unix socket at `/tmp/plugin/{plugin_name}.sock`
- Handles the FSM (Finite State Machine) protocol with Canopy
- Manages state read/write operations

### `proto/tx.proto`
Defines transaction message types:
- `Transaction` - Wrapper with signature, fee, timestamps
- `MessageSend` - Transfer tokens between addresses
- `MessageReward` - Mint tokens (admin pays fee)
- `MessageFaucet` - Mint tokens (for testing, no balance check)

## Build Commands

```bash
# Restore dependencies
make restore

# Build the plugin (framework-dependent, requires .NET runtime)
make build

# Build self-contained executable for local Linux development
# This produces a native executable that doesn't require .NET runtime installed
make build-local

# Run the plugin (starts socket server)
make run

# Run tutorial tests (requires Canopy node running)
make test-tutorial

# Clean build artifacts
make clean

# Format code
make format

# Lint code
make lint
```

### Build Variants

| Target | Output | Use Case |
|--------|--------|----------|
| `make build` | Framework-dependent DLL | Development with .NET SDK |
| `make build-local` | Self-contained executable (`bin/CanopyPlugin`) | Local Linux testing without .NET runtime |
| Release workflow | Self-contained for glibc + musl | Docker/production (handled by CI) |

## Running with Docker

The C# plugin can be run in a Docker container that includes both Canopy and the plugin.

### Build the Docker Image

From the repository root:

```bash
make docker/plugin PLUGIN=csharp
```

This builds a Docker image named `canopy-csharp` that contains:
- The Canopy binary
- The C# plugin as a self-contained executable (no .NET runtime required)
- Pre-configured `config.json` with `"plugin": "csharp"`

Note: The release workflow builds both glibc (standard Linux) and musl (Alpine) variants. The auto-update system automatically downloads the correct variant based on the runtime environment.

### Run the Container

```bash
make docker/run-csharp
```

Or manually with volume mount for persistent data:

```bash
docker run -v ~/.canopy:/root/.canopy canopy-csharp
```

### Expose Ports for Testing

To run tests against the containerized Canopy, expose the RPC ports:

```bash
docker run -p 50002:50002 -p 50003:50003 -v ~/.canopy:/root/.canopy canopy-csharp
```

| Port | Service |
|------|---------|
| 50002 | RPC API (transactions, queries) |
| 50003 | Admin RPC (keystore operations) |

Now you can run tests from your host machine that connect to `localhost:50002`.

### View Logs

```bash
# Get the container ID
docker ps

# View Canopy logs
docker exec -it <container_id> tail -f /root/.canopy/logs/log

# View plugin logs
docker exec -it <container_id> tail -f /tmp/plugin/csharp-plugin.log
```

## Development Guidelines

### Adding New Transaction Types

See `TUTORIAL.md` for the complete guide. Summary:

1. **Define protobuf message** in `proto/tx.proto`
2. **Register in ContractConfig**:
   - Add to `SupportedTransactions` array
   - Add type URL to `TransactionTypeUrls` array (same order!)
3. **Add CheckTx handler** - Stateless validation
4. **Add DeliverTx handler** - State mutation logic
5. **Rebuild** with `make build`

### Transaction Handler Pattern

```csharp
// CheckTx - Stateless validation
private PluginCheckResponse CheckMessageXxx(MessageXxx msg)
{
    // Validate addresses (must be 20 bytes)
    if (msg.SomeAddress.Length != 20)
        return new PluginCheckResponse { Error = ErrInvalidAddress() };
    
    // Validate amounts
    if (msg.Amount == 0)
        return new PluginCheckResponse { Error = ErrInvalidAmount() };
    
    // Return authorized signers
    return new PluginCheckResponse
    {
        Recipient = msg.RecipientAddress,
        AuthorizedSigners = { msg.SignerAddress }
    };
}

// DeliverTx - State mutation
private async Task<PluginDeliverResponse> DeliverMessageXxxAsync(MessageXxx msg, ulong fee)
{
    // 1. Read current state
    var response = await Plugin.StateReadAsync(this, new PluginStateReadRequest { ... });
    
    // 2. Check for errors
    if (response.Error != null)
        return new PluginDeliverResponse { Error = response.Error };
    
    // 3. Unmarshal accounts
    var account = new Account();
    account.MergeFrom(bytes);
    
    // 4. Apply state changes
    account.Amount += msg.Amount;
    
    // 5. Write state
    var writeResp = await Plugin.StateWriteAsync(this, new PluginStateWriteRequest { ... });
    return new PluginDeliverResponse { Error = writeResp.Error };
}
```

### Adding Custom RPC Endpoints

A plugin can expose its own read-only HTTP endpoints for chain-specific data (e.g. `/v1/query/faucets`, `/v1/query/rewards`). The plugin owns its HTTP server entirely — Canopy never needs to know about these routes. See `TUTORIAL.md` "Step 5b: Expose Custom RPC Endpoints" for the full walkthrough.

1. **Persist queryable records during `DeliverTx`** — write a small protobuf record to state under a plugin-owned key prefix (e.g. `Faucet` under `0x64` via `KeyForFaucet(addr)`). Endpoints can only return data that lives in state.
2. **Declare the prefix** — add every custom record prefix to `ContractConfig.CustomStatePrefixes` (in `src/CanopyPlugin/contract.cs`). `HandshakeAsync` in `src/CanopyPlugin/plugin.cs` copies these into the `PluginConfig` sent to Canopy. Canopy validates them at handshake and **panics before the plugin starts** if a prefix collides with a core-reserved prefix (`1-15`). See "Key Prefixes" below.
3. **Register routes** in `src/CanopyPlugin/rpc.cs`. The base plugin already ships a **skeleton** `StartRpcServerAsync()` that runs an `HttpListener` accept loop but registers **no routes** by default, and it is already started from `Program.cs` with `_ = plugin.StartRpcServerAsync();`. Just add a `case` per route to the `RouteRequestAsync` dispatch and implement the matching per-route handler (add as many as you want) — no change to `Program.cs` is needed.
4. **Back each handler with `QueryStateAsync`** — the detached, read-only query path: `Plugin.QueryStateAsync(height, read)` sends a `PluginQueryRequest` with a fresh random id (not tied to any in-flight tx/block) and returns raw key/value state at a historical height (`0` = latest committed). Use a single-key read (`KeyForFaucet(addr)`) for one record, or a range read over the prefix (`FaucetPrefix()`) to list all records. Decode the raw bytes into your own protobuf type and shape the JSON response however you like.
5. **Listen address** comes from the `RpcAddress` config field in `src/CanopyPlugin/config.cs` (default `0.0.0.0:50010`).

### State Keys

State keys are constructed using length-prefixed encoding:

```csharp
// Account key: 0x01 prefix + address
public static byte[] KeyForAccount(byte[] addr) => JoinLenPrefix(AccountPrefix, addr);

// Fee pool key: 0x02 prefix + chain ID
public static byte[] KeyForFeePool(ulong chainId) => JoinLenPrefix(PoolPrefix, FormatUInt64(chainId));

// Fee params key: 0x07 prefix + "/f/"
public static byte[] KeyForFeeParams() => JoinLenPrefix(ParamsPrefix, Encoding.UTF8.GetBytes("/f/"));
```

### Key Prefixes

- `0x01` (1) - Account storage (shared with core; plugins may interoperate)
- `0x02` (2) - Pool storage (shared with core; plugins may interoperate)
- `0x07` (7) - Governance parameters
- `0x64` (100) / `0x65` (101) - Example plugin-owned custom records (faucet/reward) **added by the tutorial**, not part of the base plugin

> **Avoid prefix collisions:** the plugin shares Canopy's FSM keyspace. Canopy reserves the single-byte prefixes `1-15` for its own state (accounts, pools, validators, committees, params, …). Custom plugin records **must** use prefixes outside that range (e.g. `0x64`/100, `0x65`/101). Declare them in `ContractConfig.CustomStatePrefixes`; Canopy panics at handshake — before the plugin starts — if any declared prefix collides, and the core FSM additionally rejects any write to a reserved prefix.

### BLS Signatures

The plugin uses BLS12-381 with a custom DST for compatibility with Canopy:

```csharp
// DST must match exactly
private const string DST = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";

// Sign bytes = protobuf-encoded Transaction WITHOUT signature field
var signBytes = tx.ToByteArray();  // signature field must be null/empty
var signature = BLSCrypto.Sign(secretKey, signBytes);
```

### Error Handling

Use the error factory methods in `Contract` class:

```csharp
ErrInvalidAddress()      // Code 12
ErrInvalidAmount()       // Code 13
ErrInsufficientFunds()   // Code 9
ErrInvalidMessageCast()  // Code 11
ErrTxFeeBelowStateLimit() // Code 14
```

## Testing

### Prerequisites
1. Canopy node running with C# plugin enabled (`"plugin": "csharp"` in config)
2. Plugin socket available at `/tmp/plugin/csharp-plugin.sock`

### Running Tests
```bash
cd plugin/csharp
make test
```

`make test` runs `dotnet test`, which executes both the transaction integration tests (`TestPluginTransactions`) and the custom RPC endpoints test (`TestPluginCustomRPCEndpoints`) in the tutorial test project. Use `make test-tutorial` to focus on the tutorial project with detailed logging. The custom RPC test additionally requires the plugin's RPC server to be reachable on port `50010`.

### Test Structure
The tutorial tests (`tutorial/RpcTest.cs`) demonstrate:
1. Creating accounts in keystore
2. Sending faucet transaction (mint tokens)
3. Sending transfer transaction
4. Sending reward transaction
5. Verifying balances
6. Querying the custom `/v1/query/faucets` and `/v1/query/rewards` endpoints and validating returned record structure

## RPC Endpoints

Tests communicate with Canopy via HTTP RPC:

| Endpoint | Port | Purpose |
|----------|------|---------|
| Query RPC | 50002 | `height`, `account`, `txs-by-sender`, `failed-txs`, `tx` submission |
| Admin RPC | 50003 | `keystore-new-key`, `keystore-get` |

## Common Issues

### "message name xxx is unknown"
- Check `ContractConfig.SupportedTransactions` includes the message name
- Check `ContractConfig.TransactionTypeUrls` includes the type URL
- Verify order matches between the two arrays

### Invalid signature errors
- Ensure signing protobuf bytes, not JSON
- Verify Transaction structure matches (all fields in correct order)
- Check DST matches exactly

### Plugin not connecting
- Check socket path `/tmp/plugin/csharp-plugin.sock`
- Verify plugin is running (`make run`)
- Check Canopy config has `"plugin": "csharp"`

### Build errors with test files
- The main `.csproj` excludes `test/` and `tutorial/` directories
- If new test folders are added, update the exclusion in `CanopyPlugin.csproj`

## Dependencies

- **.NET 8.0** - Target framework
- **Google.Protobuf** - Protocol Buffers runtime
- **Grpc.Tools** - Protobuf code generation
- **Nethermind.Crypto.Bls** - BLS12-381 native library (blst)

## Configuration

### `global.json`
```json
{
  "sdk": {
    "version": "8.0.100",
    "rollForward": "latestFeature"
  }
}
```

### Plugin Config in Canopy
Edit `~/.canopy/config.json`:
```json
{
  "plugin": "csharp",
  ...
}
```

## Useful Commands

```bash
# Check .NET SDK version
dotnet --version

# List available tests
dotnet test tutorial/CanopyPlugin.Tutorial.csproj --list-tests

# Run specific test
dotnet test --filter "TestPluginTransactions"

# Verbose test output
dotnet test --logger "console;verbosity=detailed"

# Check plugin logs
tail -f /tmp/plugin/csharp-plugin.log
```