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:
- Advanced commit and publish share the broad
assets:write entity. A macaroon that can call CommitVirtualPsbts / PublishAndLogTransfer necessarily also authorizes SendAsset and BurnAsset.
- 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.
- 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: false — rpcserver/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
-
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.
-
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.
-
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.
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:writeentity/action, which also grantsSendAsset,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:
assets:writeentity. A macaroon that can callCommitVirtualPsbts/PublishAndLogTransfernecessarily also authorizesSendAssetandBurnAsset.PublishAndLogTransfertakes aSkipAnchorTxBroadcastflag (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.ImportProof, lives on the dev-onlytapdevrpc.TapDevservice 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
RequiredPermissionsintaprpc/perms.go:10(there is already a maintainer TODO attaprpc/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}attaprpc/perms.go:126PublishAndLogTransfer->{assets, write}attaprpc/perms.go:130These are the same
{assets, write}op used bySendAsset(taprpc/perms.go:83) andBurnAsset(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:rpcserver/rpcserver.go:3396req.SkipAnchorTxBroadcastforwarded intotapfreighter.NewPreAnchoredParcel(...)atrpcserver/rpcserver.go:3486Proof verification is already read-scoped and separate, which is good and should be preserved:
VerifyProof->{proofs, read}attaprpc/perms.go:67, handler atrpcserver/rpcserver.go:2154. (Note: the handler intentionally does not error on an invalid proof; it returnsValid: false—rpcserver/rpcserver.go:2171-2178.)Proof import is the sharp edge:
ImportProof->{proofs, write}attaprpc/perms.go:350, but it is on the/tapdevrpc.TapDev/service, handler atrpcserver/rpcserver.go:2501.TapDevgRPC service is only registered under thedevbuild tag:taprpc/tapdevrpc/config_active.gois//go:build devand callsRegisterTapDevServer, whiletaprpc/tapdevrpc/config_default.gois//go:build !devand itsRegisterGrpcServeris 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.RegisterTransfer({assets, write}+{proofs, write}attaprpc/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
Introduce a dedicated entity for custom-anchor authoring so it can be baked separately from
assets. Suggested entity nameanchor(ortransfers), with:CommitVirtualPsbts->{anchor, write}(replacing{assets, write}attaprpc/perms.go:126).PublishAndLogTransfer->{anchor, write}(replacing{assets, write}attaprpc/perms.go:130).This lets an operator grant commit+publish without granting
SendAsset/BurnAsset.Split log-only publish from broadcasting publish. Because
SkipAnchorTxBroadcastis a request field rather than a separate method, the static per-method map intaprpc/perms.gocannot express this on its own. Two viable approaches — maintainer to choose:{anchor, write}; whenreq.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 ofPublishAndLogTransfer(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 lndmacaroonsinterceptor wiring.LogTransfer) that never broadcasts, mapped to a narrower op, leavingPublishAndLogTransferfor the broadcasting path. Cleaner for the static map but adds RPC surface.Provide a stable, permissionable proof-import path. Promote a raw-proof-blob import out of the dev-only
tapdevrpc.TapDevservice (registered only under//go:build dev,taprpc/tapdevrpc/config_default.go) into a non-dev RPC (e.g. onassetwalletrpc.AssetWalletortaprpc.TaprootAssets), gated on{proofs, write}, so a release-build tapd can accept an externally-produced proof file. Alternatively, extendRegisterTransferto optionally accept the proof blob directly instead of requiring it to pre-exist in the local universe (rpcserver/rpcserver.go:11375-11382). KeepVerifyProofat{proofs, read}(already correct attaprpc/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:writecredential 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 becauseImportProofis dev-tag-only andRegisterTransferrequires the proof to already be in the local universe. These are backend permission-granularity gaps the SDK cannot paper over on its own.