fix: improve ipfs name put for IPNS record republishing (#11199)
Some checks failed
Docker Check / lint (push) Waiting to run
Docker Check / build (push) Waiting to run
Gateway Conformance / gateway-conformance (push) Waiting to run
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Waiting to run
Go Build / go-build (push) Waiting to run
Go Check / go-check (push) Waiting to run
Go Lint / go-lint (push) Waiting to run
Go Test / unit-tests (push) Waiting to run
Go Test / cli-tests (push) Waiting to run
Go Test / example-tests (push) Waiting to run
Interop / interop-prep (push) Waiting to run
Interop / helia-interop (push) Blocked by required conditions
Interop / ipfs-webui (push) Blocked by required conditions
Sharness / sharness-test (push) Waiting to run
Spell Check / spellcheck (push) Waiting to run
CodeQL / codeql (push) Has been cancelled

`name put` rejected republishing the exact same record because the
sequence check used `>=` which blocked the common use case of fetching
a third-party record and putting it back to refresh DHT availability.

allow putting identical records (same bytes) while still rejecting
different records with the same or lower sequence number. also add
a success message on put (suppressible with `--quiet`), and clarify
the error message to say "IPNS record" and reference `ipfs name put --force`.

Closes #11197
This commit is contained in:
Marcin Rataj 2026-02-16 19:52:30 +01:00 committed by GitHub
parent 1c6f924c78
commit 3ba73501fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 119 additions and 13 deletions

View File

@ -333,6 +333,7 @@ const (
forceOptionName = "force"
putAllowOfflineOption = "allow-offline"
allowDelegatedOption = "allow-delegated"
putQuietOptionName = "quiet"
maxIPNSRecordSize = 10 << 10 // 10 KiB per IPNS spec
)
@ -374,6 +375,7 @@ By default, the command validates that:
- The record size is within 10 KiB limit
- The signature matches the provided IPNS name
- The record's sequence number is higher than any existing record
(identical records are allowed for republishing)
The --force flag skips this command's validation and passes the record
directly to the routing system. Note that --force only affects this command;
@ -421,6 +423,7 @@ Force store a record to test routing validation:
cmds.BoolOption(forceOptionName, "f", "Skip validation (signature, sequence, size)."),
cmds.BoolOption(putAllowOfflineOption, "Store locally without broadcasting to the network."),
cmds.BoolOption(allowDelegatedOption, "Publish via HTTP delegated publishers only (no DHT)."),
cmds.BoolOption(putQuietOptionName, "q", "Write no output."),
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
nd, err := cmdenv.GetNode(env)
@ -506,14 +509,15 @@ Force store a record to test routing validation:
// Check for sequence conflicts with existing record
existingData, err := api.Routing().Get(req.Context, nameArg)
if err == nil {
// We have an existing record, check sequence
existingRec, parseErr := ipns.UnmarshalRecord(existingData)
if parseErr == nil {
existingSeq, seqErr := existingRec.Sequence()
newSeq, newSeqErr := rec.Sequence()
if seqErr == nil && newSeqErr == nil {
if existingSeq >= newSeq {
return fmt.Errorf("existing record has sequence %d >= new record sequence %d, use --force to overwrite", existingSeq, newSeq)
// Allow republishing the exact same record (common use case:
// get a third-party record and put it back to refresh DHT)
if !bytes.Equal(existingData, data) {
existingRec, parseErr := ipns.UnmarshalRecord(existingData)
if parseErr == nil {
existingSeq, seqErr := existingRec.Sequence()
newSeq, newSeqErr := rec.Sequence()
if seqErr == nil && newSeqErr == nil && existingSeq >= newSeq {
return fmt.Errorf("existing IPNS record has sequence %d >= new record sequence %d, use 'ipfs name put --force' to skip this check", existingSeq, newSeq)
}
}
}
@ -536,6 +540,28 @@ Force store a record to test routing validation:
return err
}
return nil
// Extract value from the record for the response
value := ""
if rec, err := ipns.UnmarshalRecord(data); err == nil {
if v, err := rec.Value(); err == nil {
value = v.String()
}
}
return cmds.EmitOnce(res, &IpnsEntry{
Name: name.String(),
Value: value,
})
},
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, ie *IpnsEntry) error {
quiet, _ := req.Options[putQuietOptionName].(bool)
if quiet {
return nil
}
_, err := fmt.Fprintln(w, cmdenv.EscNonPrint(ie.Name))
return err
}),
},
Type: IpnsEntry{},
}

View File

@ -587,10 +587,9 @@ func TestNameGetPut(t *testing.T) {
res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
require.NoError(t, res.Err)
// now try to put the same record again (should fail - same sequence)
// put the same record again (identical record republishing is allowed)
res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
require.Error(t, res.Err)
require.Contains(t, res.Stderr.String(), "existing record has sequence")
require.NoError(t, res.Err)
// put the record with --force (should succeed)
res = node.RunIPFS("name", "put", "--force", ipnsName.String(), recordFile)
@ -884,6 +883,87 @@ func TestNameGetPut(t *testing.T) {
res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
require.Error(t, res.Err)
require.Contains(t, res.Stderr.String(), "existing record has sequence 200 >= new record sequence 100")
require.Contains(t, res.Stderr.String(), "existing IPNS record has sequence 200 >= new record sequence 100")
})
t.Run("name put allows identical record republishing", func(t *testing.T) {
t.Parallel()
h := harness.NewT(t)
publishPath := "/ipfs/" + fixtureCid
ipnsName, record := makeExternalRecord(t, h, publishPath, "--sequence=100")
node := makeDaemon(t)
defer node.StopDaemon()
// put the record
res := node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
require.NoError(t, res.Err)
// put the exact same record again (same bytes, same sequence)
// this should succeed: republishing an identical record is a valid use case
res = node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
require.NoError(t, res.Err)
require.Contains(t, res.Stdout.String(), ipnsName.String())
})
t.Run("name put rejects different record with same sequence", func(t *testing.T) {
t.Parallel()
h := harness.NewT(t)
// create two different records signed by the same key with the same
// sequence number by using two ephemeral nodes that share a key
ephNode1 := h.NewNode().Init("--profile=test")
r, err := os.Open(fixturePath)
require.NoError(t, err)
err = ephNode1.IPFSDagImport(r, fixtureCid)
r.Close()
require.NoError(t, err)
ephNode1.StartDaemon()
res := ephNode1.IPFS("key", "gen", "--type=ed25519", "shared-key")
keyID := strings.TrimSpace(res.Stdout.String())
ipnsName, err := ipns.NameFromString(keyID)
require.NoError(t, err)
// publish record A (sequence=100, value=fixtureCid)
ephNode1.IPFS("name", "publish", "--key=shared-key", "--lifetime=5m", "--sequence=100", "/ipfs/"+fixtureCid)
res = ephNode1.IPFS("name", "get", ipnsName.String())
recordA := res.Stdout.Bytes()
// export key and import into second ephemeral node
keyFile := filepath.Join(ephNode1.Dir, "shared-key.key")
ephNode1.IPFS("key", "export", "--output="+keyFile, "shared-key")
ephNode1.StopDaemon()
ephNode2 := h.NewNode().Init("--profile=test")
ephNode2.StartDaemon()
ephNode2.IPFS("key", "import", "shared-key", keyFile)
// publish record B (sequence=100, different value)
ephNode2.IPFS("name", "publish", "--key=shared-key", "--lifetime=5m", "--sequence=100", "/ipfs/bafkqaaa")
res = ephNode2.IPFS("name", "get", ipnsName.String())
recordB := res.Stdout.Bytes()
ephNode2.StopDaemon()
// verify records have same sequence but different bytes
require.NotEqual(t, recordA, recordB, "records should have different bytes")
// start test node and try the put scenario
node := makeDaemon(t)
defer node.StopDaemon()
// put record A
res = node.PipeToIPFS(bytes.NewReader(recordA), "name", "put", ipnsName.String())
require.NoError(t, res.Err)
// try to put record B (different bytes, same sequence=100)
recordFile := filepath.Join(node.Dir, "recordB.bin")
err = os.WriteFile(recordFile, recordB, 0644)
require.NoError(t, err)
res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
require.Error(t, res.Err)
require.Contains(t, res.Stderr.String(), "existing IPNS record has sequence 100 >= new record sequence 100")
})
}