mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
fix: improve ipfs name put for IPNS record republishing
`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:
parent
8eab2fcf5d
commit
e03c3ec8ce
@ -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{},
|
||||
}
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user