Section 03

Implementation

Core protocol guides and code examples for builders integrating Urma claims into clients and mirrors. Backend-specific guides are in the Backends section.

1. Install

The Urma Go library provides core types, encoding/decoding, and schema generation. Backend implementations are separate modules.

shell
go get go.lumeweb.com/urma

2. Encode a Claim

Construct a UrmaClaim, serialize it as JSON, and wrap it in the appropriate envelope. The codec handles size validation against the 8192-byte on-chain limit.

encode.go
import "go.lumeweb.com/urma"

claim := urma.UrmaClaim{
    Version:       0,
    Location:      "sia",  // backend-specific identifier
    SourceClaimID: decodeHex("a1b2c3d4e5f60102030405060708090a0b0c0d0e"),
    DataKey:       [32]byte{66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    LocationData:  rootLocationData,  // backend-specific opaque JSON
}

// Encode to JSON (validates size budget)
jsonBytes, err := urma.EncodeClaim(claim)
if err != nil {
    log.Fatal(err)
}

// Wrap in unsigned envelope for global Urma
envelope := urma.EncodeUnsignedEnvelope(jsonBytes)

// Or wrap in signed envelope for creator-owned Urma
// signedEnv, err := urma.EncodeSignedEnvelope(jsonBytes, channelClaimID, privKey, txInput0Hash)

3. Decode & Verify

On discovery, decode the envelope, parse the JSON payload, and run the validation pipeline. Verify sourceClaimId matches the expected value computed from the source claim.

decode.go
// rawValue is the claim value bytes from getclaimsforname
claimJSON, version, channelClaimID, signature, err := urma.DecodeEnvelope(rawValue)
if err != nil {
    // Invalid envelope: skip this claim
    return
}

claim, err := urma.DecodeClaim(claimJSON)
if err != nil {
    return
}

// Verify sourceClaimId matches expected value
if claim.SourceClaimID != expectedClaimID {
    return // mismatched claim
}

// For signed envelopes, verify the channel signature
if version == urma.EnvelopeSigned {
    if err := urma.VerifySignedEnvelope(rawValue, txInput0Hash, channelPubKey); err != nil {
        return // invalid signature
    }
}

// Fetch root locationData from storage backend
// Then follow the manifest page chain to collect all blobs

infoValidation Order

Filters are ordered by cost: zero-cost checks first (status, JSON decode, sourceClaimId match), then signature verification, then network fetches last.

4. Extending: New Storage Backend

The protocol is storage-agnostic. To add a new backend, define the manifest types and page chain construction for that backend's storage model. Core types use opaque JSON for locationData, data, and next. The backend decides what goes inside them.

backend.go
// 1. Choose a location identifier string
const LocationMyBackend = "mybackend"

// 2. Define the locationData schema for the root pointer
//    This is opaque JSON: whatever your backend needs to find the first manifest page
type MyBackendLocationData struct {
    // e.g. a CID, a URL, a slab slice, etc.
}

// 3. Define manifest page payload types
type MyBackendManifest struct {
    // Blob entries, stream metadata, etc.
}

// 4. Implement page chain construction
//    Pages link in reverse: last page first, first page last.
//    Each page's `next` field references the previous page's storage location.

infoNote

Core types (UrmaClaim, ManifestPage) remain unchanged. New backends only define their own locationData schema and manifest payload types. See the Sia backend for a complete implementation reference.