Blog

Canonical References

Published by fintohaps on 12.08.2025

Canonical what?

Most people who use Git are used to referring to things called branches (refs/heads). “I pushed the branch”, “check out the feature branch to see the work”, “Ok, I’ll pull your branch”, etc. A branch is simply a specific kind of reference in a Git. You’re also likely familiar with tags (refs/tags), and these are another kind of references, too! A reference, quite simply, is a name that points to a Git object, like a commit object or a tag object. The naming convention and what objects they point to allow us to talk more specifically about branches, remote branches, tags, and notes.

So what’s a canonical reference? In the Radicle protocol, all your references live under your namespace (refs/namespaces/<nid>/refs/{head,tags,*}). We want Radicle repositories to also act like more traditional Git repositories that have the typical references refs/heads/main, refs/tags/v1.0.0, refs/heads/qa/feature-1. So we introduced a way to synthesise these references from the references under the set of refs/namespaces/<nid>/refs. These references become canon1 and can be considered for use, e.g. fetching a specific tag object for building binaries.

Git History in the Making

Since the dawn of the heartwood implementation of the Radicle protocol, the concept of the defaultBranch was included in the identity document. Alongside this, we have had the threshold value. Even more importantly, we have had the delegates managing the identity of the identity document, as well. By combining these 3 values we were able to calculate the canonical reference of the defaultBranch. The defaultBranch becomes a canonical reference when a threshold number of delegates decide on a commit to use for its canonical form.

If you have been using Radicle, you’ll have noticed that whenever you push Git tags that they get placed under your namespace. Most people who have used git would expect these tags to be placed under refs/tags. Up until now, we have broken this expectation, because we are working in a decentralised environment using Radicle, protecting the global namespace of the repository. So, we asked ourselves, “Can the defaultBranch approach be generalised?” This way, the refs/tags namespace can be populated using the canonical form of the tag.

“It should be easy!”, I said – famous last words.

What ensued was a RIP2 and two patches34, over a time period that felt like forever. It turned out that evolving things in a protocol can be quite tricky when it comes to compatibility. You may notice that the merged patch and the RIP will differ slightly in the approach recommended. For now, the approach we are using bases itself on the extensible payloads in the identity document.

After all that, we are proud to announce that you can now start using canonical references, so let’s go through how to use them.

A Quick Guide to Canonical References

As alluded to above, we need three values to specify a rule for a canonical reference. We need a reference the rule applies to, a set of allowed DIDs (currently always did:keys), and a threshold. These three values form a canonical reference rule. In fact, we can generalise the reference value to use a Git reference pattern5.

So, where do these new rules go? The new update of Radicle will now interpret a new payload identified by the key xyz.radicle.crefs. Under this key, it expects a rules key, which in turn can hold a number of canonical reference rules that are identified by their Git reference pattern.

For each rule, there are two keys: allow and threshold – these refer to the allowed set of DIDs and the threshold value respectively.

To demonstrate, the maintainers of the heartwood repository have prepared a rule already so that we can use release tags. I (@fintohaps) used the command rad id update --edit to open up my editor with the identity document pre-populated. I added the new payload with a rule for refs/tags/releases/*, the allowed set of the current 3 maintainers of the repository, and a threshold of 2. Here is what the document looked like when saving:

{
  "payload": {
    "xyz.radicle.project": {
      "defaultBranch": "master",
      "description": "Radicle Heartwood Protocol & Stack",
      "name": "heartwood"
    },
    "xyz.radicle.crefs": {
      "rules": {
        "refs/tags/releases/*": {
          "allow": [
            "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
            "did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
            "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
          ],
          "threshold": 2
        }
      }
    }
  },
  "delegates": [
    "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
    "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
    "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
    "did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
    "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
  ],
  "threshold": 1
}

And here’s the output of the rad id command:

╭───────────────────────────────────────────────────────────────────────────────────╮
│ Title    Add Release Tag Canonical Refs Rule                                      │
│ Revision 45e43cc54284f579deb7ae64e4d162274c04fa3b                                 │
│ Blob     def6df9a842e68525b371462200b6d5454269601                                 │
│ Author   did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM                 │
│ State    active                                                                   │
│ Quorum   no                                                                       │
│                                                                                   │
│ Adding the `xyz.radicle.crefs` payload that starts with a single for the          │
│ `refs/tags/releases/*` rules that uses Erik, Fintan, and Lorenz as the allowed    │
│ delegates, and a threshold of 2.                                                  │
├───────────────────────────────────────────────────────────────────────────────────┤
│ ✓ did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM fintohaps        (you) │
│ ? did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT cloudhead              │
│ ? did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW cloudhead-laptop       │
│ ? did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz erikli                 │
│ ✓ did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz lorenz                 │
╰───────────────────────────────────────────────────────────────────────────────────╯

@@ -1,17 +1,29 @@
 {
   "payload": {
+    "xyz.radicle.crefs": {
+      "rules": {
+        "refs/tags/releases/*": {
+          "allow": [
+            "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
+            "did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
+            "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
+          ],
+          "threshold": 2
+        }
+      }
+    },
     "xyz.radicle.project": {
       "defaultBranch": "master",
       "description": "Radicle Heartwood Protocol & Stack",
       "name": "heartwood"
     }
   },
   "delegates": [
     "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
     "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
     "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
     "did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
     "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
   ],
   "threshold": 1
 }

You can look at the evolution of the proposal by using rad id show 45e43cc54284f579deb7ae64e4d162274c04fa3b in the heartwood repository.

Note that since the rules are being added to the identity document, it is necessary to reach a majority quorum for proposing these changes.

Once the rules are in place, any time you push or fetch a reference that matches a rule, it will compute the canonical form for that reference. In the case above, if any of the 2 allowed did:keys agree on a tag that lives under their namespace as refs/tags/releases, then a corresponding refs/tags will be computed.

This means that there will be more top-level references appearing in Radicle repositories, and importantly, tooling and processes that require Git tags can now take advantage of Radicle repositories!

Now, it’s time to go try out this feature and report back to us! Release tags to your hearts content ❤️🌱

  1. Here we are using the term in the sense that some content made may not be considered part of the canon of some story or universe. For example, some Star Wars content may be considered as canon by George Lucas, but some may not be. 

  2. RIP-4 Canonical References 

  3. First patch attempting to implement canonical references 

  4. Second patch implementing canonical references using the identity payload 

  5. Git ref format and reference patterns