Skip to content

taprpc: granular macaroon permissions for advanced custom-anchor commit, log-only publish, and proof import #2213

Description

@darioAnongba

Problem

Hosted / multi-tenant deployments that drive the advanced custom-anchor flow (a caller owns the BTC anchor transaction, asks tapd to commit assets into it, signs externally, then publishes/logs) cannot scope a macaroon to just that flow. The custom-anchor RPCs collapse into the single coarse assets:write entity/action, which also grants SendAsset, BurnAsset, FundVirtualPsbt, SignVirtualPsbt, AnchorVirtualPsbts, NextInternalKey/NextScriptKey, ProveAssetOwnership, backup import/export, etc. There is no way to hand an integration a credential that can commit-and-log against an externally-owned anchor without also handing it full custody-level send/burn authority.

Three specific gaps:

  1. Advanced commit and publish share the broad assets:write entity. A macaroon that can call CommitVirtualPsbts / PublishAndLogTransfer necessarily also authorizes SendAsset and BurnAsset.
  2. Log-only publish is indistinguishable from broadcast. PublishAndLogTransfer takes a SkipAnchorTxBroadcast flag (log the transfer + ship proofs, but do not broadcast), but both the log-only and the actually-broadcasting variants require the exact same permission. A deployment that wants to allow "record this transfer and ship proofs, but never let this credential put a transaction on-chain" cannot express that.
  3. Proof import has no stable, permissionable RPC. The only RPC that ingests a raw proof-file blob, ImportProof, lives on the dev-only tapdevrpc.TapDev service and is not registered in a release build at all (see below). The production alternative, RegisterTransfer, does not accept an arbitrary proof blob.

The net effect is that a least-privilege integration either over-grants (assets:write) or cannot perform the operation at all (proof import in a non-dev build).

Current behaviour

The permission map is RequiredPermissions in taprpc/perms.go:10 (there is already a maintainer TODO at taprpc/perms.go:9: "re think these and go instead w/ the * approach?").

Relevant entries, all mapping the custom-anchor operations onto the same broad assets:write:

  • CommitVirtualPsbts -> {assets, write} at taprpc/perms.go:126
  • PublishAndLogTransfer -> {assets, write} at taprpc/perms.go:130

These are the same {assets, write} op used by SendAsset (taprpc/perms.go:83) and BurnAsset (taprpc/perms.go:87), so the entity cannot separate custom-anchor authoring from custody-level spends.

PublishAndLogTransfer's handler passes the request's skip-broadcast flag straight through to the freighter parcel with no permission differentiation between the log-only and broadcasting paths:

  • handler: rpcserver/rpcserver.go:3396
  • req.SkipAnchorTxBroadcast forwarded into tapfreighter.NewPreAnchoredParcel(...) at rpcserver/rpcserver.go:3486

Proof verification is already read-scoped and separate, which is good and should be preserved:

  • VerifyProof -> {proofs, read} at taprpc/perms.go:67, handler at rpcserver/rpcserver.go:2154. (Note: the handler intentionally does not error on an invalid proof; it returns Valid: falserpcserver/rpcserver.go:2171-2178.)

Proof import is the sharp edge:

  • ImportProof -> {proofs, write} at taprpc/perms.go:350, but it is on the /tapdevrpc.TapDev/ service, handler at rpcserver/rpcserver.go:2501.
  • The TapDev gRPC service is only registered under the dev build tag: taprpc/tapdevrpc/config_active.go is //go:build dev and calls RegisterTapDevServer, while taprpc/tapdevrpc/config_default.go is //go:build !dev and its RegisterGrpcServer is a no-op. So in a release/default build the method is not served at all — the macaroon entry exists but the RPC is unreachable.
  • The production-facing alternative RegisterTransfer ({assets, write} + {proofs, write} at taprpc/perms.go:107) does not accept a raw proof-file blob: it looks the proof up in the local universe and requires the script key to already be declared (rpcserver/rpcserver.go:11375-11382), and its own comment calls out that this design is deliberately unlike "the old tapdevrpc.ImportProof RPC" (rpcserver/rpcserver.go:11373-11374).

Existing macaroon entities (for reference, distinct values in the map): addresses, assets, channels, daemon, macaroon, mailbox, mint, proofs, rfq, universe.

Proposed change

  1. Introduce a dedicated entity for custom-anchor authoring so it can be baked separately from assets. Suggested entity name anchor (or transfers), with:

    • CommitVirtualPsbts -> {anchor, write} (replacing {assets, write} at taprpc/perms.go:126).
    • PublishAndLogTransfer -> {anchor, write} (replacing {assets, write} at taprpc/perms.go:130).
      This lets an operator grant commit+publish without granting SendAsset/BurnAsset.
  2. Split log-only publish from broadcasting publish. Because SkipAnchorTxBroadcast is a request field rather than a separate method, the static per-method map in taprpc/perms.go cannot express this on its own. Two viable approaches — maintainer to choose:

    • (a) Enforce inside the handler: base method requires {anchor, write}; when req.SkipAnchorTxBroadcast == false (i.e. the credential is asking tapd to actually broadcast), additionally require a stronger op such as {onchain, write} or {assets, write}. The extra check would go at the top of PublishAndLogTransfer (rpcserver/rpcserver.go:3396), reading the caller's macaroon ops from the context. The exact plumbing to read granted ops in the handler needs confirmation against the lnd macaroons interceptor wiring.
    • (b) Add a separate log-only RPC method (e.g. LogTransfer) that never broadcasts, mapped to a narrower op, leaving PublishAndLogTransfer for the broadcasting path. Cleaner for the static map but adds RPC surface.
  3. Provide a stable, permissionable proof-import path. Promote a raw-proof-blob import out of the dev-only tapdevrpc.TapDev service (registered only under //go:build dev, taprpc/tapdevrpc/config_default.go) into a non-dev RPC (e.g. on assetwalletrpc.AssetWallet or taprpc.TaprootAssets), gated on {proofs, write}, so a release-build tapd can accept an externally-produced proof file. Alternatively, extend RegisterTransfer to optionally accept the proof blob directly instead of requiring it to pre-exist in the local universe (rpcserver/rpcserver.go:11375-11382). Keep VerifyProof at {proofs, read} (already correct at taprpc/perms.go:67) so verification and import are separately grantable.

Context

Surfaced while building the advanced custom-anchor transaction builder in lightninglabs/tap-sdk#158 (used by SwapDK). The SDK fails closed today: it will not run the custom-anchor commit/publish flow with an over-broad assets:write credential where a hosted operator wants least privilege, it cannot offer a log-only-publish credential that is provably unable to broadcast, and it cannot import a raw proof blob against a release-build tapd because ImportProof is dev-tag-only and RegisterTransfer requires the proof to already be in the local universe. These are backend permission-granularity gaps the SDK cannot paper over on its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status
    🆕 New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions