@cryptotaxi247 / kubo / commits / d81f524cc

feat(ipns): support passing custom sequence number during publishing (#10851)

* feat(ipns): Add a parameter in name.publish to change the sequence number * test: monotonic name publish --sequence * docs: `name publish --sequence` * chore: boxo main with PR 962 --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>

Sergey Gorbunov committed Aug 13, 2025 at 05:15 UTC d81f524cce2bc6cc75d6a9165b9f39b05a4114bc
12 files changed +120 -10
.github/workflows/interop.yml
+1 -1
@@ -91,7 +91,7 @@ jobs:
91 steps:
92 - uses: actions/setup-node@v4
93 with:
94 - node-version: 18.14.0
94 + node-version: 20.x
95 - uses: actions/download-artifact@v4
96 with:
97 name: kubo
core/commands/name/publish.go
+23
@@ -27,6 +27,7 @@ const (
27 keyOptionName = "key"
28 quieterOptionName = "quieter"
29 v1compatOptionName = "v1compat"
30 + sequenceOptionName = "sequence"
31 )
32
33 var PublishCmd = &cmds.Command{
@@ -66,6 +67,23 @@ Alternatively, publish an <ipfs-path> using a valid PeerID (as listed by
67 > ipfs name publish --key=QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
68 Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
69
70 +Notes:
71 +
72 +The --ttl option specifies the time duration for caching IPNS records.
73 +Lower values like '1m' enable faster updates but increase network load,
74 +while the default of 1 hour reduces traffic but may delay propagation.
75 +Gateway operators may override this with Ipns.MaxCacheTTL configuration.
76 +
77 +The --sequence option sets a custom sequence number for the IPNS record.
78 +The sequence number must be monotonically increasing (greater than the
79 +current record's sequence). This is useful for manually coordinating
80 +updates across multiple writers. If not specified, the sequence number
81 +increments automatically.
82 +
83 +For faster IPNS updates, consider:
84 +- Using a lower --ttl value (e.g., '1m' for quick updates)
85 +- Enabling PubSub via Ipns.UsePubsub in the config
86 +
87 `,
88 },
89
@@ -80,6 +98,7 @@ Alternatively, publish an <ipfs-path> using a valid PeerID (as listed by
98 cmds.BoolOption(quieterOptionName, "Q", "Write only final IPNS Name encoded as CIDv1 (for use in /ipns content paths)."),
99 cmds.BoolOption(v1compatOptionName, "Produce a backward-compatible IPNS Record by including fields for both V1 and V2 signatures.").WithDefault(true),
100 cmds.BoolOption(allowOfflineOptionName, "When --offline, save the IPNS record to the local datastore without broadcasting to the network (instead of failing)."),
101 + cmds.Uint64Option(sequenceOptionName, "Set a custom sequence number for the IPNS record (must be higher than current)."),
102 ke.OptionIPNSBase,
103 },
104 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
@@ -114,6 +133,10 @@ Alternatively, publish an <ipfs-path> using a valid PeerID (as listed by
133 opts = append(opts, options.Name.TTL(d))
134 }
135
136 + if sequence, found := req.Options[sequenceOptionName].(uint64); found {
137 + opts = append(opts, options.Name.Sequence(sequence))
138 + }
139 +
140 p, err := cmdutils.PathOrCidPath(req.Arguments[0])
141 if err != nil {
142 return err
core/coreapi/name.go
+4
@@ -66,6 +66,10 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam
66 publishOptions = append(publishOptions, namesys.PublishWithTTL(*options.TTL))
67 }
68
69 + if options.Sequence != nil {
70 + publishOptions = append(publishOptions, namesys.PublishWithSequence(*options.Sequence))
71 + }
72 +
73 err = api.namesys.Publish(ctx, k, p, publishOptions...)
74 if err != nil {
75 return ipns.Name{}, err
core/coreiface/options/name.go
+10
@@ -16,6 +16,7 @@ type NamePublishSettings struct {
16 TTL *time.Duration
17 CompatibleWithV1 bool
18 AllowOffline bool
19 + Sequence *uint64
20 }
21
22 type NameResolveSettings struct {
@@ -105,6 +106,15 @@ func (nameOpts) TTL(ttl time.Duration) NamePublishOption {
106 }
107 }
108
109 +// Sequence is an option for Name.Publish which specifies the sequence number of
110 +// a namesys record.
111 +func (nameOpts) Sequence(seq uint64) NamePublishOption {
112 + return func(settings *NamePublishSettings) error {
113 + settings.Sequence = &seq
114 + return nil
115 + }
116 +}
117 +
118 // CompatibleWithV1 is an option for [Name.Publish] which specifies if the
119 // created record should be backwards compatible with V1 IPNS Records.
120 func (nameOpts) CompatibleWithV1(compatible bool) NamePublishOption {
docs/changelogs/v0.37.md
+6
@@ -13,8 +13,10 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
13 - [Clear provide queue when reprovide strategy changes](#clear-provide-queue-when-reprovide-strategy-changes)
14 - [🪵 Revamped `ipfs log level` command](#-revamped-ipfs-log-level-command)
15 - [📌 Named pins in `ipfs add` command](#-named-pins-in-ipfs-add-command)
16 + - [Custom sequence numbers in `ipfs name publish`](#custom-sequence-numbers-in-ipfs-name-publish)
17 - [⚙️ `Reprovider.Strategy` is now consistently respected](#-reprovider-strategy-is-now-consistently-respected)
18 - [Removed unnecessary dependencies](#removed-unnecessary-dependencies)
19 + - [Improved `ipfs cid`](#improved-ipfs-cid)
20 - [Deprecated `ipfs stats reprovide`](#deprecated-ipfs-stats-reprovide)
21 - [🔄 AutoRelay now uses all connected peers for relay discovery](#-autorelay-now-uses-all-connected-peers-for-relay-discovery)
22 - [📦️ Important dependency updates](#-important-dependency-updates)
@@ -93,6 +95,10 @@ Kubo has been cleaned up by removing unnecessary dependencies and packages:
95
96 These changes reduce the dependency footprint while improving code maintainability and following Go best practices.
97
98 +#### Custom sequence numbers in `ipfs name publish`
99 +
100 +Added `--sequence` flag to `ipfs name publish` for setting custom sequence numbers in IPNS records. This enables advanced use cases like manually coordinating updates across multiple nodes. See `ipfs name publish --help` for details.
101 +
102 #### Improved `ipfs cid`
103
104 Certain `ipfs cid` commands can now be run without a daemon or repository, and return correct exit code 1 on error, making it easier to perform CID conversion in scripts and CI/CD pipelines.
docs/examples/kubo-as-a-library/go.mod
+1 -1
@@ -7,7 +7,7 @@ go 1.24
7 replace github.com/ipfs/kubo => ./../../..
8
9 require (
10 - github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08
10 + github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e
11 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
12 github.com/libp2p/go-libp2p v0.43.0
13 github.com/multiformats/go-multiaddr v0.16.1
docs/examples/kubo-as-a-library/go.sum
+2 -2
@@ -287,8 +287,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd
287 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU=
288 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
289 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
290 -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 h1:PtntQQtYOh7YTCRnrU1idTuOwxEi0ZmYM4u7ZfSAExY=
291 -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08/go.mod h1:KwlJTzv5fb1GLlA9KyMqHQmvP+4mrFuiE3PnjdrPJHs=
290 +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e h1:A2zSzpyrerCtdN69iDxt9S9z27cD1R4Uw3l1ctLTxX0=
291 +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e/go.mod h1:ehi6uM9NBRkAaB7Q7u2kZgGArXPfbNRe0X/CYTqUwq8=
292 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
293 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
294 github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=
go.mod
+1 -1
@@ -22,7 +22,7 @@ require (
22 github.com/hashicorp/go-version v1.7.0
23 github.com/ipfs-shipyard/nopfs v0.0.14
24 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0
25 - github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08
25 + github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e
26 github.com/ipfs/go-block-format v0.2.2
27 github.com/ipfs/go-cid v0.5.0
28 github.com/ipfs/go-cidutil v0.1.0
go.sum
+2 -2
@@ -354,8 +354,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd
354 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU=
355 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
356 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
357 -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 h1:PtntQQtYOh7YTCRnrU1idTuOwxEi0ZmYM4u7ZfSAExY=
358 -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08/go.mod h1:KwlJTzv5fb1GLlA9KyMqHQmvP+4mrFuiE3PnjdrPJHs=
357 +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e h1:A2zSzpyrerCtdN69iDxt9S9z27cD1R4Uw3l1ctLTxX0=
358 +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e/go.mod h1:ehi6uM9NBRkAaB7Q7u2kZgGArXPfbNRe0X/CYTqUwq8=
359 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
360 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
361 github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=
test/cli/name_test.go
+67
@@ -263,4 +263,71 @@ func TestName(t *testing.T) {
263 require.NoError(t, err)
264 require.False(t, val.Validation.Valid)
265 })
266 +
267 + t.Run("Publishing with custom sequence number", func(t *testing.T) {
268 + t.Parallel()
269 +
270 + node := makeDaemon(t, nil)
271 + publishPath := "/ipfs/" + fixtureCid
272 + name := ipns.NameFromPeer(node.PeerID())
273 +
274 + t.Run("Publish with sequence=0 is not allowed", func(t *testing.T) {
275 + // Sequence=0 is never valid, even on a fresh node
276 + res := node.RunIPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=0", publishPath)
277 + require.NotEqual(t, 0, res.ExitCode(), "Expected publish with sequence=0 to fail")
278 + require.Contains(t, res.Stderr.String(), "sequence number must be greater than the current record sequence")
279 + })
280 +
281 + t.Run("Publish with sequence=1 on fresh node", func(t *testing.T) {
282 + // Sequence=1 is the minimum valid sequence number for first publish
283 + res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=1", publishPath)
284 + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
285 + })
286 +
287 + t.Run("Publish with sequence=42", func(t *testing.T) {
288 + res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=42", publishPath)
289 + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
290 + })
291 +
292 + t.Run("Publish with large sequence number", func(t *testing.T) {
293 + res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=18446744073709551615", publishPath) // Max uint64
294 + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
295 + })
296 + })
297 +
298 + t.Run("Sequence number monotonic check", func(t *testing.T) {
299 + t.Parallel()
300 +
301 + node := makeDaemon(t, nil).StartDaemon()
302 + publishPath1 := "/ipfs/" + fixtureCid
303 + publishPath2 := "/ipfs/" + dagCid // Different content
304 + name := ipns.NameFromPeer(node.PeerID())
305 +
306 + // First, publish with a high sequence number (1000)
307 + res := node.IPFS("name", "publish", "--ttl=0", "--sequence=1000", publishPath1)
308 + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath1), res.Stdout.String())
309 +
310 + // Verify the record was published successfully
311 + res = node.IPFS("name", "resolve", name.String())
312 + require.Contains(t, res.Stdout.String(), publishPath1)
313 +
314 + // Now try to publish different content with a LOWER sequence number (500)
315 + // This should fail due to monotonic sequence check
316 + res = node.RunIPFS("name", "publish", "--ttl=0", "--sequence=500", publishPath2)
317 + require.NotEqual(t, 0, res.ExitCode(), "Expected publish with lower sequence to fail")
318 + require.Contains(t, res.Stderr.String(), "sequence number", "Expected error about sequence number")
319 +
320 + // Verify the original content is still published (not overwritten)
321 + res = node.IPFS("name", "resolve", name.String())
322 + require.Contains(t, res.Stdout.String(), publishPath1, "Original content should still be published")
323 + require.NotContains(t, res.Stdout.String(), publishPath2, "New content should not have been published")
324 +
325 + // Publishing with a HIGHER sequence number should succeed
326 + res = node.IPFS("name", "publish", "--ttl=0", "--sequence=2000", publishPath2)
327 + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath2), res.Stdout.String())
328 +
329 + // Verify the new content is now published
330 + res = node.IPFS("name", "resolve", name.String())
331 + require.Contains(t, res.Stdout.String(), publishPath2, "New content should now be published")
332 + })
333 }
test/dependencies/go.mod
+1 -1
@@ -129,7 +129,7 @@ require (
129 github.com/huin/goupnp v1.3.0 // indirect
130 github.com/inconshreveable/mousetrap v1.1.0 // indirect
131 github.com/ipfs/bbloom v0.0.4 // indirect
132 - github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 // indirect
132 + github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e // indirect
133 github.com/ipfs/go-bitfield v1.1.0 // indirect
134 github.com/ipfs/go-block-format v0.2.2 // indirect
135 github.com/ipfs/go-cid v0.5.0 // indirect
test/dependencies/go.sum
+2 -2
@@ -321,8 +321,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
321 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
322 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
323 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
324 -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 h1:PtntQQtYOh7YTCRnrU1idTuOwxEi0ZmYM4u7ZfSAExY=
325 -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08/go.mod h1:KwlJTzv5fb1GLlA9KyMqHQmvP+4mrFuiE3PnjdrPJHs=
324 +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e h1:A2zSzpyrerCtdN69iDxt9S9z27cD1R4Uw3l1ctLTxX0=
325 +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e/go.mod h1:ehi6uM9NBRkAaB7Q7u2kZgGArXPfbNRe0X/CYTqUwq8=
326 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
327 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
328 github.com/ipfs/go-block-format v0.2.2 h1:uecCTgRwDIXyZPgYspaLXoMiMmxQpSx2aq34eNc4YvQ=