diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index e85e1f2fe..af49f27f2 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -91,7 +91,7 @@ jobs: steps: - uses: actions/setup-node@v4 with: - node-version: 18.14.0 + node-version: 20.x - uses: actions/download-artifact@v4 with: name: kubo diff --git a/core/commands/name/publish.go b/core/commands/name/publish.go index 168d7fb44..db97b3b13 100644 --- a/core/commands/name/publish.go +++ b/core/commands/name/publish.go @@ -27,6 +27,7 @@ const ( keyOptionName = "key" quieterOptionName = "quieter" v1compatOptionName = "v1compat" + sequenceOptionName = "sequence" ) var PublishCmd = &cmds.Command{ @@ -66,6 +67,23 @@ Alternatively, publish an using a valid PeerID (as listed by > ipfs name publish --key=QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy +Notes: + +The --ttl option specifies the time duration for caching IPNS records. +Lower values like '1m' enable faster updates but increase network load, +while the default of 1 hour reduces traffic but may delay propagation. +Gateway operators may override this with Ipns.MaxCacheTTL configuration. + +The --sequence option sets a custom sequence number for the IPNS record. +The sequence number must be monotonically increasing (greater than the +current record's sequence). This is useful for manually coordinating +updates across multiple writers. If not specified, the sequence number +increments automatically. + +For faster IPNS updates, consider: +- Using a lower --ttl value (e.g., '1m' for quick updates) +- Enabling PubSub via Ipns.UsePubsub in the config + `, }, @@ -80,6 +98,7 @@ Alternatively, publish an using a valid PeerID (as listed by cmds.BoolOption(quieterOptionName, "Q", "Write only final IPNS Name encoded as CIDv1 (for use in /ipns content paths)."), cmds.BoolOption(v1compatOptionName, "Produce a backward-compatible IPNS Record by including fields for both V1 and V2 signatures.").WithDefault(true), cmds.BoolOption(allowOfflineOptionName, "When --offline, save the IPNS record to the local datastore without broadcasting to the network (instead of failing)."), + cmds.Uint64Option(sequenceOptionName, "Set a custom sequence number for the IPNS record (must be higher than current)."), ke.OptionIPNSBase, }, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { @@ -114,6 +133,10 @@ Alternatively, publish an using a valid PeerID (as listed by opts = append(opts, options.Name.TTL(d)) } + if sequence, found := req.Options[sequenceOptionName].(uint64); found { + opts = append(opts, options.Name.Sequence(sequence)) + } + p, err := cmdutils.PathOrCidPath(req.Arguments[0]) if err != nil { return err diff --git a/core/coreapi/name.go b/core/coreapi/name.go index 305c19e43..b43f9def6 100644 --- a/core/coreapi/name.go +++ b/core/coreapi/name.go @@ -66,6 +66,10 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam publishOptions = append(publishOptions, namesys.PublishWithTTL(*options.TTL)) } + if options.Sequence != nil { + publishOptions = append(publishOptions, namesys.PublishWithSequence(*options.Sequence)) + } + err = api.namesys.Publish(ctx, k, p, publishOptions...) if err != nil { return ipns.Name{}, err diff --git a/core/coreiface/options/name.go b/core/coreiface/options/name.go index 7b4b6a8fd..d62c427ad 100644 --- a/core/coreiface/options/name.go +++ b/core/coreiface/options/name.go @@ -16,6 +16,7 @@ type NamePublishSettings struct { TTL *time.Duration CompatibleWithV1 bool AllowOffline bool + Sequence *uint64 } type NameResolveSettings struct { @@ -105,6 +106,15 @@ func (nameOpts) TTL(ttl time.Duration) NamePublishOption { } } +// Sequence is an option for Name.Publish which specifies the sequence number of +// a namesys record. +func (nameOpts) Sequence(seq uint64) NamePublishOption { + return func(settings *NamePublishSettings) error { + settings.Sequence = &seq + return nil + } +} + // CompatibleWithV1 is an option for [Name.Publish] which specifies if the // created record should be backwards compatible with V1 IPNS Records. func (nameOpts) CompatibleWithV1(compatible bool) NamePublishOption { diff --git a/docs/changelogs/v0.37.md b/docs/changelogs/v0.37.md index 63a1501d1..165d30a59 100644 --- a/docs/changelogs/v0.37.md +++ b/docs/changelogs/v0.37.md @@ -13,8 +13,10 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. - [Clear provide queue when reprovide strategy changes](#clear-provide-queue-when-reprovide-strategy-changes) - [🪵 Revamped `ipfs log level` command](#-revamped-ipfs-log-level-command) - [📌 Named pins in `ipfs add` command](#-named-pins-in-ipfs-add-command) + - [Custom sequence numbers in `ipfs name publish`](#custom-sequence-numbers-in-ipfs-name-publish) - [⚙️ `Reprovider.Strategy` is now consistently respected](#-reprovider-strategy-is-now-consistently-respected) - [Removed unnecessary dependencies](#removed-unnecessary-dependencies) + - [Improved `ipfs cid`](#improved-ipfs-cid) - [Deprecated `ipfs stats reprovide`](#deprecated-ipfs-stats-reprovide) - [🔄 AutoRelay now uses all connected peers for relay discovery](#-autorelay-now-uses-all-connected-peers-for-relay-discovery) - [📦️ Important dependency updates](#-important-dependency-updates) @@ -93,6 +95,10 @@ Kubo has been cleaned up by removing unnecessary dependencies and packages: These changes reduce the dependency footprint while improving code maintainability and following Go best practices. +#### Custom sequence numbers in `ipfs name publish` + +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. + #### Improved `ipfs cid` 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. diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index a4decfb45..00ce3d806 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -7,7 +7,7 @@ go 1.24 replace github.com/ipfs/kubo => ./../../.. require ( - github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 + github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 github.com/libp2p/go-libp2p v0.43.0 github.com/multiformats/go-multiaddr v0.16.1 diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 821d8f8da..dace80e15 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -287,8 +287,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 h1:PtntQQtYOh7YTCRnrU1idTuOwxEi0ZmYM4u7ZfSAExY= -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08/go.mod h1:KwlJTzv5fb1GLlA9KyMqHQmvP+4mrFuiE3PnjdrPJHs= +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e h1:A2zSzpyrerCtdN69iDxt9S9z27cD1R4Uw3l1ctLTxX0= +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e/go.mod h1:ehi6uM9NBRkAaB7Q7u2kZgGArXPfbNRe0X/CYTqUwq8= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk= diff --git a/go.mod b/go.mod index 0c0310df6..2e75ec217 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/ipfs-shipyard/nopfs v0.0.14 github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 - github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 + github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e github.com/ipfs/go-block-format v0.2.2 github.com/ipfs/go-cid v0.5.0 github.com/ipfs/go-cidutil v0.1.0 diff --git a/go.sum b/go.sum index a1d9c953f..6bb8d9801 100644 --- a/go.sum +++ b/go.sum @@ -354,8 +354,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 h1:PtntQQtYOh7YTCRnrU1idTuOwxEi0ZmYM4u7ZfSAExY= -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08/go.mod h1:KwlJTzv5fb1GLlA9KyMqHQmvP+4mrFuiE3PnjdrPJHs= +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e h1:A2zSzpyrerCtdN69iDxt9S9z27cD1R4Uw3l1ctLTxX0= +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e/go.mod h1:ehi6uM9NBRkAaB7Q7u2kZgGArXPfbNRe0X/CYTqUwq8= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk= diff --git a/test/cli/name_test.go b/test/cli/name_test.go index 42c649c09..460f5f1a6 100644 --- a/test/cli/name_test.go +++ b/test/cli/name_test.go @@ -263,4 +263,71 @@ func TestName(t *testing.T) { require.NoError(t, err) require.False(t, val.Validation.Valid) }) + + t.Run("Publishing with custom sequence number", func(t *testing.T) { + t.Parallel() + + node := makeDaemon(t, nil) + publishPath := "/ipfs/" + fixtureCid + name := ipns.NameFromPeer(node.PeerID()) + + t.Run("Publish with sequence=0 is not allowed", func(t *testing.T) { + // Sequence=0 is never valid, even on a fresh node + res := node.RunIPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=0", publishPath) + require.NotEqual(t, 0, res.ExitCode(), "Expected publish with sequence=0 to fail") + require.Contains(t, res.Stderr.String(), "sequence number must be greater than the current record sequence") + }) + + t.Run("Publish with sequence=1 on fresh node", func(t *testing.T) { + // Sequence=1 is the minimum valid sequence number for first publish + res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=1", publishPath) + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String()) + }) + + t.Run("Publish with sequence=42", func(t *testing.T) { + res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=42", publishPath) + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String()) + }) + + t.Run("Publish with large sequence number", func(t *testing.T) { + res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=18446744073709551615", publishPath) // Max uint64 + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String()) + }) + }) + + t.Run("Sequence number monotonic check", func(t *testing.T) { + t.Parallel() + + node := makeDaemon(t, nil).StartDaemon() + publishPath1 := "/ipfs/" + fixtureCid + publishPath2 := "/ipfs/" + dagCid // Different content + name := ipns.NameFromPeer(node.PeerID()) + + // First, publish with a high sequence number (1000) + res := node.IPFS("name", "publish", "--ttl=0", "--sequence=1000", publishPath1) + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath1), res.Stdout.String()) + + // Verify the record was published successfully + res = node.IPFS("name", "resolve", name.String()) + require.Contains(t, res.Stdout.String(), publishPath1) + + // Now try to publish different content with a LOWER sequence number (500) + // This should fail due to monotonic sequence check + res = node.RunIPFS("name", "publish", "--ttl=0", "--sequence=500", publishPath2) + require.NotEqual(t, 0, res.ExitCode(), "Expected publish with lower sequence to fail") + require.Contains(t, res.Stderr.String(), "sequence number", "Expected error about sequence number") + + // Verify the original content is still published (not overwritten) + res = node.IPFS("name", "resolve", name.String()) + require.Contains(t, res.Stdout.String(), publishPath1, "Original content should still be published") + require.NotContains(t, res.Stdout.String(), publishPath2, "New content should not have been published") + + // Publishing with a HIGHER sequence number should succeed + res = node.IPFS("name", "publish", "--ttl=0", "--sequence=2000", publishPath2) + require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath2), res.Stdout.String()) + + // Verify the new content is now published + res = node.IPFS("name", "resolve", name.String()) + require.Contains(t, res.Stdout.String(), publishPath2, "New content should now be published") + }) } diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index cfc9c7c24..19571aff4 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -129,7 +129,7 @@ require ( github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect - github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 // indirect + github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e // indirect github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-block-format v0.2.2 // indirect github.com/ipfs/go-cid v0.5.0 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 21ad5cf0a..19f6ee6ef 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -321,8 +321,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08 h1:PtntQQtYOh7YTCRnrU1idTuOwxEi0ZmYM4u7ZfSAExY= -github.com/ipfs/boxo v0.33.2-0.20250804224807-e5da058ebb08/go.mod h1:KwlJTzv5fb1GLlA9KyMqHQmvP+4mrFuiE3PnjdrPJHs= +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e h1:A2zSzpyrerCtdN69iDxt9S9z27cD1R4Uw3l1ctLTxX0= +github.com/ipfs/boxo v0.33.2-0.20250813013451-825361b44b4e/go.mod h1:ehi6uM9NBRkAaB7Q7u2kZgGArXPfbNRe0X/CYTqUwq8= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.2.2 h1:uecCTgRwDIXyZPgYspaLXoMiMmxQpSx2aq34eNc4YvQ=