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=