@cryptotaxi247 / kubo / commits / 3ba73501f

fix: improve `ipfs name put` for IPNS record republishing (#11199)

`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

Marcin Rataj committed Feb 16, 2026 at 19:52 UTC 3ba73501fe91e1d4a4cff6960c822a891c573de8
2 files changed +119 -13
core/commands/name/name.go
+35 -9
@@ -333,6 +333,7 @@ const (
333 forceOptionName = "force"
334 putAllowOfflineOption = "allow-offline"
335 allowDelegatedOption = "allow-delegated"
336 + putQuietOptionName = "quiet"
337 maxIPNSRecordSize = 10 << 10 // 10 KiB per IPNS spec
338 )
339
@@ -374,6 +375,7 @@ By default, the command validates that:
375 - The record size is within 10 KiB limit
376 - The signature matches the provided IPNS name
377 - The record's sequence number is higher than any existing record
378 + (identical records are allowed for republishing)
379
380 The --force flag skips this command's validation and passes the record
381 directly to the routing system. Note that --force only affects this command;
@@ -421,6 +423,7 @@ Force store a record to test routing validation:
423 cmds.BoolOption(forceOptionName, "f", "Skip validation (signature, sequence, size)."),
424 cmds.BoolOption(putAllowOfflineOption, "Store locally without broadcasting to the network."),
425 cmds.BoolOption(allowDelegatedOption, "Publish via HTTP delegated publishers only (no DHT)."),
426 + cmds.BoolOption(putQuietOptionName, "q", "Write no output."),
427 },
428 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
429 nd, err := cmdenv.GetNode(env)
@@ -506,14 +509,15 @@ Force store a record to test routing validation:
509 // Check for sequence conflicts with existing record
510 existingData, err := api.Routing().Get(req.Context, nameArg)
511 if err == nil {
509 - // We have an existing record, check sequence
510 - existingRec, parseErr := ipns.UnmarshalRecord(existingData)
511 - if parseErr == nil {
512 - existingSeq, seqErr := existingRec.Sequence()
513 - newSeq, newSeqErr := rec.Sequence()
514 - if seqErr == nil && newSeqErr == nil {
515 - if existingSeq >= newSeq {
516 - return fmt.Errorf("existing record has sequence %d >= new record sequence %d, use --force to overwrite", existingSeq, newSeq)
512 + // Allow republishing the exact same record (common use case:
513 + // get a third-party record and put it back to refresh DHT)
514 + if !bytes.Equal(existingData, data) {
515 + existingRec, parseErr := ipns.UnmarshalRecord(existingData)
516 + if parseErr == nil {
517 + existingSeq, seqErr := existingRec.Sequence()
518 + newSeq, newSeqErr := rec.Sequence()
519 + if seqErr == nil && newSeqErr == nil && existingSeq >= newSeq {
520 + return fmt.Errorf("existing IPNS record has sequence %d >= new record sequence %d, use 'ipfs name put --force' to skip this check", existingSeq, newSeq)
521 }
522 }
523 }
@@ -536,6 +540,28 @@ Force store a record to test routing validation:
540 return err
541 }
542
539 - return nil
543 + // Extract value from the record for the response
544 + value := ""
545 + if rec, err := ipns.UnmarshalRecord(data); err == nil {
546 + if v, err := rec.Value(); err == nil {
547 + value = v.String()
548 + }
549 + }
550 +
551 + return cmds.EmitOnce(res, &IpnsEntry{
552 + Name: name.String(),
553 + Value: value,
554 + })
555 + },
556 + Encoders: cmds.EncoderMap{
557 + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, ie *IpnsEntry) error {
558 + quiet, _ := req.Options[putQuietOptionName].(bool)
559 + if quiet {
560 + return nil
561 + }
562 + _, err := fmt.Fprintln(w, cmdenv.EscNonPrint(ie.Name))
563 + return err
564 + }),
565 },
566 + Type: IpnsEntry{},
567 }
test/cli/name_test.go
+84 -4
@@ -587,10 +587,9 @@ func TestNameGetPut(t *testing.T) {
587 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
588 require.NoError(t, res.Err)
589
590 - // now try to put the same record again (should fail - same sequence)
590 + // put the same record again (identical record republishing is allowed)
591 res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
592 - require.Error(t, res.Err)
593 - require.Contains(t, res.Stderr.String(), "existing record has sequence")
592 + require.NoError(t, res.Err)
593
594 // put the record with --force (should succeed)
595 res = node.RunIPFS("name", "put", "--force", ipnsName.String(), recordFile)
@@ -884,6 +883,87 @@ func TestNameGetPut(t *testing.T) {
883
884 res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
885 require.Error(t, res.Err)
887 - require.Contains(t, res.Stderr.String(), "existing record has sequence 200 >= new record sequence 100")
886 + require.Contains(t, res.Stderr.String(), "existing IPNS record has sequence 200 >= new record sequence 100")
887 + })
888 +
889 + t.Run("name put allows identical record republishing", func(t *testing.T) {
890 + t.Parallel()
891 + h := harness.NewT(t)
892 + publishPath := "/ipfs/" + fixtureCid
893 +
894 + ipnsName, record := makeExternalRecord(t, h, publishPath, "--sequence=100")
895 +
896 + node := makeDaemon(t)
897 + defer node.StopDaemon()
898 +
899 + // put the record
900 + res := node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
901 + require.NoError(t, res.Err)
902 +
903 + // put the exact same record again (same bytes, same sequence)
904 + // this should succeed: republishing an identical record is a valid use case
905 + res = node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
906 + require.NoError(t, res.Err)
907 + require.Contains(t, res.Stdout.String(), ipnsName.String())
908 + })
909 +
910 + t.Run("name put rejects different record with same sequence", func(t *testing.T) {
911 + t.Parallel()
912 + h := harness.NewT(t)
913 +
914 + // create two different records signed by the same key with the same
915 + // sequence number by using two ephemeral nodes that share a key
916 + ephNode1 := h.NewNode().Init("--profile=test")
917 + r, err := os.Open(fixturePath)
918 + require.NoError(t, err)
919 + err = ephNode1.IPFSDagImport(r, fixtureCid)
920 + r.Close()
921 + require.NoError(t, err)
922 + ephNode1.StartDaemon()
923 +
924 + res := ephNode1.IPFS("key", "gen", "--type=ed25519", "shared-key")
925 + keyID := strings.TrimSpace(res.Stdout.String())
926 + ipnsName, err := ipns.NameFromString(keyID)
927 + require.NoError(t, err)
928 +
929 + // publish record A (sequence=100, value=fixtureCid)
930 + ephNode1.IPFS("name", "publish", "--key=shared-key", "--lifetime=5m", "--sequence=100", "/ipfs/"+fixtureCid)
931 + res = ephNode1.IPFS("name", "get", ipnsName.String())
932 + recordA := res.Stdout.Bytes()
933 +
934 + // export key and import into second ephemeral node
935 + keyFile := filepath.Join(ephNode1.Dir, "shared-key.key")
936 + ephNode1.IPFS("key", "export", "--output="+keyFile, "shared-key")
937 + ephNode1.StopDaemon()
938 +
939 + ephNode2 := h.NewNode().Init("--profile=test")
940 + ephNode2.StartDaemon()
941 + ephNode2.IPFS("key", "import", "shared-key", keyFile)
942 +
943 + // publish record B (sequence=100, different value)
944 + ephNode2.IPFS("name", "publish", "--key=shared-key", "--lifetime=5m", "--sequence=100", "/ipfs/bafkqaaa")
945 + res = ephNode2.IPFS("name", "get", ipnsName.String())
946 + recordB := res.Stdout.Bytes()
947 + ephNode2.StopDaemon()
948 +
949 + // verify records have same sequence but different bytes
950 + require.NotEqual(t, recordA, recordB, "records should have different bytes")
951 +
952 + // start test node and try the put scenario
953 + node := makeDaemon(t)
954 + defer node.StopDaemon()
955 +
956 + // put record A
957 + res = node.PipeToIPFS(bytes.NewReader(recordA), "name", "put", ipnsName.String())
958 + require.NoError(t, res.Err)
959 +
960 + // try to put record B (different bytes, same sequence=100)
961 + recordFile := filepath.Join(node.Dir, "recordB.bin")
962 + err = os.WriteFile(recordFile, recordB, 0644)
963 + require.NoError(t, err)
964 +
965 + res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
966 + require.Error(t, res.Err)
967 + require.Contains(t, res.Stderr.String(), "existing IPNS record has sequence 100 >= new record sequence 100")
968 })
969 }