Explanations

About Anchor Forwarding


Introduction

Anchor forwarding lets a ledger delegate anchor operations to a Bridge. This is useful when anchors are managed by an external system (for example, an external alias directory) while the ledger remains the public API surface and source of authorization decisions.

Before diving in, a brief note on alias directories. An alias directory is a service that maps human‑readable identifiers (like phone numbers or emails) to payment credentials. In deployments where an external alias directory (Bridge) owns anchors, anchor forwarding allows the ledger to forward create/update/sign/drop/read operations to that Bridge while still enforcing access rules and signing responses.

What gets forwarded

Ledger can forward the following anchor actions:

  • Create
  • Update
  • Sign (add a proof)
  • Drop (delete)
  • Get (read one)
  • FindAll (get many with filters)

Forwarding is controlled by configuration per action and, optionally, by a forwarding strategy.

Configuration

Set the Bridge handle that should receive forwarded requests. You can update the active ledger config with CLI, for example using minka ledger update -e.

{
  "forward.anchor.create": "MyBridgeHandle",
  "forward.anchor.update": "MyBridgeHandle",
  "forward.anchor.drop": "MyBridgeHandle",
  "forward.anchor.sign": "MyBridgeHandle",
  "forward.anchor.get": "MyBridgeHandle",
  "forward.anchor.findAll": "MyBridgeHandle"
}

Optionally configure a global or per‑action strategy:

{
  "forward.anchor.strategy": "proxy | fallback | validate | none",
  "forward.anchor.create.strategy": "proxy | validate | none",
  "forward.anchor.update.strategy": "proxy | validate | none",
  "forward.anchor.drop.strategy": "proxy | validate | none",
  "forward.anchor.get.strategy": "proxy | fallback | none",
  "forward.anchor.sign.strategy": "proxy | validate | none",
  "forward.anchor.findAll.strategy": "proxy | fallback | none"
}
  • proxy: Forward the request and do not persist or mutate the record locally (proxy‑only).
  • fallback: For reads/lists, return local data when present; otherwise call the Bridge.
  • validate: Attempt the local DB operation first (without committing) to ensure it would succeed, forward to the Bridge, then persist locally if the Bridge succeeds. If the Bridge fails, do not mutate locally.
  • none: Do not forward for this action.

If a per‑action strategy is not set, the global forward.anchor.strategy is applied. If no strategy is provided, sensible defaults are used internally: reads default to fallback; writes default to validate.

Bridge endpoints

The Bridge must expose the same API surface as the ledger for anchors. Requests are forwarded as‑is (with some headers filtered and a ledger authorization added), and responses are expected to match the ledger Anchor API.

Required endpoints:

  • POST /v2/anchors
  • PUT /v2/anchors/:id
  • DELETE /v2/anchors/:id
  • GET /v2/anchors/:id
  • POST /v2/anchors/:id/proofs (sign)
  • GET /v2/anchors (findAll with filters)

Responses must conform to the ledger Anchor API. See the Anchor section in the API reference.

What the ledger forwards

  • Request body/query params: Forwarded unchanged.
  • Headers: Custom client headers are forwarded except a forbidden set. The original client Authorization (if present) is moved to x-forwarded-authorization and is not sent as authorization to the Bridge.
  • Ledger authorization: The ledger adds its own Bearer JWT in Authorization, signed by the ledger. The JWT contains claims identifying the ledger and the intended Bridge audience.

Forbidden headers that are not forwarded include: authorization, x-ledger, content-type, content-length, accept, accept-encoding, caller, connection, host.

Responses and errors

Success responses

The ledger returns the response from the Bridge after verifying hash and proofs are consistent:

  • Proxy mode: Returns the Bridge response directly to the client.
  • Validate mode: Returns the result of the local operation to the client (after confirming the Bridge call succeeded).
  • FindAll queries: The entire page body from the Bridge is forwarded.

Error handling

When a forwarded request to a Bridge fails, the ledger interprets and returns Bridge errors with meaningful structure whenever possible. The handling depends on the error type, status code, and response structure.

Error handling summary table

CaseBridge ResponseBridge Status CodeLedger Response Status CodeLedger Error ReasonNotes
Valid bridge errorWell-formed error with data (containing reason, detail, optional custom), meta, hash, and valid proofsAny (e.g., 400, 404, 422)Same as bridgeSame as bridge data.reasonBridge error is forwarded to client with ledger signature added
Missing fieldsMissing data, meta, or hashAny502forward.invalid-responseResponse structure is incomplete
Invalid data structuredata missing required reason or detail fieldsAny502forward.invalid-responseData doesn't conform to ledger error schema
Invalid data typesreason or detail not strings, or custom not a plain objectAny502forward.invalid-responseData fields have wrong types
Extra fields in datadata contains fields other than reason, detail, customAny502forward.invalid-responseData has unevaluated properties
Invalid hash or proofsHash doesn't match data or proofs fail verificationAny502forward.invalid-responseCryptographic validation failed
Ledger at faultAny response structure401 or 403500forward.unexpected-errorLedger credentials or config issue
Connection errorNetwork, timeout, or code errorN/A500forward.unexpected-errorUnexpected system error

Detailed error cases with examples

1. Valid bridge error (forwarded to client)

When the Bridge returns a well-formed error response, the ledger validates the structure and cryptographic integrity, then forwards the error to the client with the Bridge's original status code.

Bridge response (status 404):

{
  "data": {
    "reason": "record.not-found",
    "detail": "Anchor with handle 'missing-anchor' not found",
    "custom": {
      "searchedHandle": "missing-anchor",
      "bridgeContext": "alias-directory"
    }
  },
  "hash": "7f3e8a2b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
  "meta": {
    "moment": "2025-11-04T10:30:00.000Z",
    "proofs": [{
      "custom": {
        "moment": "2025-11-04T10:30:00.000Z"
      },
      "method": "ed25519-v2",
      "result": "abc123...",
      "public": "bridge-public-key",
      "digest": "def456..."
    }]
  }
}

Ledger response to client (status 404):

{
  "data": {
    "reason": "record.not-found",
    "detail": "Anchor with handle 'missing-anchor' not found",
    "custom": {
      "searchedHandle": "missing-anchor",
      "bridgeContext": "alias-directory"
    }
  },
  "hash": "7f3e8a2b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
  "meta": {
    "moment": "2025-11-04T10:30:01.000Z",
    "proofs": [
      {
        "custom": {
          "moment": "2025-11-04T10:30:00.000Z"
        },
        "method": "ed25519-v2",
        "result": "abc123...",
        "public": "bridge-public-key",
        "digest": "def456..."
      },
      {
        "custom": {
          "moment": "2025-11-04T10:30:01.000Z",
          "causedBy": {
            "detail": "Error derived from anchor forwarding response"
          }
        },
        "method": "ed25519-v2",
        "result": "xyz789...",
        "public": "ledger-public-key",
        "digest": "ghi012..."
      }
    ]
  }
}

Key points:

  • Status code is preserved (404)
  • Bridge error reason and detail are forwarded as-is
  • Bridge custom data is preserved in data.custom
  • Ledger appends its own proof to the meta.proofs array
  • Ledger adds causedBy context in its proof's custom field
  • Hash remains the same (hash of the original data)
2. Invalid bridge response - Missing required fields

Bridge response (status 400):

{
  "error": {
    "code": "INVALID_ANCHOR",
    "message": "Anchor data is invalid"
  }
}

Ledger response to client (status 502):

{
  "data": {
    "reason": "forward.invalid-response",
    "detail": "Invalid response from bridge test-bridge"
  },
  "hash": "...",
  "meta": {
    "moment": "2025-11-04T10:30:01.000Z",
    "proofs": [{
      "custom": {
        "moment": "2025-11-04T10:30:01.000Z"
      },
      "method": "ed25519-v2",
      "result": "...",
      "public": "ledger-public-key",
      "digest": "..."
    }]
  }
}

Key points:

  • Status code changes to 502 (Bad Gateway)
  • Ledger generates its own error with reason forward.invalid-response
  • Bridge response doesn't contain required data, meta, hash structure
3. Invalid bridge response - Wrong data structure

Bridge response (status 400):

{
  "data": {
    "errorCode": "ANCHOR_EXISTS",
    "message": "Anchor already exists"
  },
  "hash": "...",
  "meta": {
    "proofs": [...]
  }
}

Ledger response to client (status 502):

{
  "data": {
    "reason": "forward.invalid-response",
    "detail": "Invalid response from bridge test-bridge"
  },
  "hash": "...",
  "meta": {
    "moment": "2025-11-04T10:30:01.000Z",
    "proofs": [{
      "custom": {
        "moment": "2025-11-04T10:30:01.000Z"
      },
      "method": "ed25519-v2",
      "result": "...",
      "public": "ledger-public-key",
      "digest": "..."
    }]
  }
}

Key points:

  • Status code changes to 502
  • Bridge data object must contain reason and detail fields (not errorCode and message)
  • Only reason, detail, and custom fields are allowed in data
4. Invalid bridge response - Invalid hash or proofs

Bridge response (status 422):

{
  "data": {
    "reason": "record.invalid",
    "detail": "Anchor validation failed"
  },
  "hash": "incorrect-hash-value",
  "meta": {
    "proofs": [{
      "method": "ed25519-v2",
      "result": "invalid-signature",
      "public": "bridge-public-key",
      "digest": "..."
    }]
  }
}

Ledger response to client (status 502):

{
  "data": {
    "reason": "forward.invalid-response",
    "detail": "Invalid response from bridge test-bridge"
  },
  "hash": "...",
  "meta": {
    "moment": "2025-11-04T10:30:01.000Z",
    "proofs": [{
      "custom": {
        "moment": "2025-11-04T10:30:01.000Z"
      },
      "method": "ed25519-v2",
      "result": "...",
      "public": "ledger-public-key",
      "digest": "..."
    }]
  }
}

Key points:

  • Status code changes to 502
  • Ledger validates that hash matches the hash of data
  • Ledger validates that proofs are valid for the given hash
  • If validation fails, returns forward.invalid-response
5. Ledger credentials error (401/403)

Bridge response (status 401):

{
  "data": {
    "reason": "auth.unauthorized",
    "detail": "Invalid JWT signature"
  },
  "hash": "...",
  "meta": {
    "proofs": [...]
  }
}

Ledger response to client (status 500):

{
  "data": {
    "reason": "forward.unexpected-error",
    "detail": "Unexpected error while forwarding request to bridge"
  },
  "hash": "...",
  "meta": {
    "moment": "2025-11-04T10:30:01.000Z",
    "proofs": [{
      "custom": {
        "moment": "2025-11-04T10:30:01.000Z"
      },
      "method": "ed25519-v2",
      "result": "...",
      "public": "ledger-public-key",
      "digest": "..."
    }]
  }
}

Key points:

  • Bridge returns 401 or 403, indicating ledger's credentials are invalid
  • Status code changes to 500 (Internal Server Error)
  • Ledger treats this as its own fault, not a bridge business error
6. Connection error (network, timeout, etc.)

Example: Network connection timeout, DNS resolution failure, or code exception.

Ledger response to client (status 500):

{
  "data": {
    "reason": "forward.unexpected-error",
    "detail": "Unexpected error while forwarding request to bridge"
  },
  "hash": "...",
  "meta": {
    "moment": "2025-11-04T10:30:01.000Z",
    "proofs": [{
      "custom": {
        "moment": "2025-11-04T10:30:01.000Z"
      },
      "method": "ed25519-v2",
      "result": "...",
      "public": "ledger-public-key",
      "digest": "..."
    }]
  }
}

Key points:

  • Any error that's not from the bridge (HTTP response errors)
  • Status code is 500
  • Indicates system-level error, not a business error

Additional notes

  • No local mutation on Bridge failure: In all write operations, if the Bridge returns any kind of error, the operation is not persisted or mutated locally, regardless of strategy.
  • Bridge error schema requirements: For errors to be properly forwarded, Bridges must return error responses using the ledger error schema with data.reason, data.detail, and optional data.custom fields.
  • Proof validation: All Bridge responses (both success and error) must include valid cryptographic proofs that the ledger can verify.

See the Anchor API Spec Reference for additional details on error response schemas.

Access checks and validation

  • Writes (create/update/sign/drop): The ledger validates authorization before any forwarding occurs. It also performs local pre‑checks to ensure the DB operation would succeed (insert/update/sign/drop) when using the validate strategy.
  • Reads (get/findAll): The ledger validates authorization on returned data. With fallback, the ledger will return local data when available; otherwise it fetches from the Bridge and then validates proofs and access before returning.
  • Schema and proofs: Returned anchors are validated for proofs integrity. In validate strategy, local state is also verified before commit.

Proofs added by the ledger when forwarding

When forwarding write operations, the ledger appends a proof with custom.status = "forwarded" to the outgoing meta before calling the Bridge. The Bridge response must still contain valid proofs; the ledger will verify them before returning to the client (or before persisting, in validate mode).

Flows

Create

Update

Sign

Drop

Get by id/handle

Get many (findAll)

Security notes

  • The client Authorization header is not forwarded as authorization; it is copied to x-forwarded-authorization for the Bridge to inspect if needed.
  • The ledger signs forwarded requests by adding its own Bearer JWT in authorization. Bridges can validate the signature with the ledger public key and may also require OAuth on top if desired.
  • Standard Bridge authentication can be configured separately (e.g., OAuth 2.0). If a Bridge uses OAuth, the ledger will still send its own JWT while preserving the client token in x-forwarded-authorization.
  • All communication between ledger and Bridge must use TLS.

Operational notes

  • Errors from the Bridge are surfaced to clients with ledger signatures and without persisting local mutations.
  • In proxy strategy, records are not stored locally. Reads will come from the Bridge and writes will only affect the Bridge.
  • In validate strategy, local operations are performed only after the Bridge confirms success.