ipns record selection via sequence numbers
This commit adds a sequence number to the IpnsEntry protobuf that is used to determine which among a set of entries for the same key is the 'most correct'. GetValues has been added to the routing interface to retrieve a set of records from the dht, for the caller to select from. GetValue (singular) will call GetValues, select the 'best' record, and then update that record to peers we received outdated records from. This will help keep the dht consistent. License: MIT Signed-off-by: Jeromy <jeromyj@gmail.com>
Jeromy committed
Sep 18, 2015 at 10:27 UTC
cfcc3d6a1b77a47cdf6512e6be24e8b3bbd9fb05
15 files changed
+301
-37
core/core.go
+1
@@ -501,6 +501,7 @@ func startListening(ctx context.Context, host p2phost.Host, cfg *config.Config)
501
func constructDHTRouting(ctx context.Context, host p2phost.Host, dstore ds.ThreadSafeDatastore) (routing.IpfsRouting, error) {
502
dhtRouting := dht.NewDHT(ctx, host, dstore)
503
dhtRouting.Validator[IpnsValidatorTag] = namesys.IpnsRecordValidator
504
+ dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
505
return dhtRouting, nil
506
}
507
namesys/pb/namesys.pb.go
+8
@@ -56,6 +56,7 @@ type IpnsEntry struct {
56
Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"`
57
ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=namesys.pb.IpnsEntry_ValidityType" json:"validityType,omitempty"`
58
Validity []byte `protobuf:"bytes,4,opt,name=validity" json:"validity,omitempty"`
59
+ Sequence *uint64 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"`
60
XXX_unrecognized []byte `json:"-"`
61
}
62
@@ -91,6 +92,13 @@ func (m *IpnsEntry) GetValidity() []byte {
92
return nil
93
}
94
95
+func (m *IpnsEntry) GetSequence() uint64 {
96
+ if m != nil && m.Sequence != nil {
97
+ return *m.Sequence
98
+ }
99
+ return 0
100
+}
101
+
102
func init() {
103
proto.RegisterEnum("namesys.pb.IpnsEntry_ValidityType", IpnsEntry_ValidityType_name, IpnsEntry_ValidityType_value)
104
}
namesys/pb/namesys.proto
+2
@@ -10,4 +10,6 @@ message IpnsEntry {
10
11
optional ValidityType validityType = 3;
12
optional bytes validity = 4;
13
+
14
+ optional uint64 sequence = 5;
15
}
namesys/publisher.go
+70
-7
@@ -7,6 +7,7 @@ import (
7
"time"
8
9
proto "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/gogo/protobuf/proto"
10
+ ds "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-datastore"
11
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
12
13
key "github.com/ipfs/go-ipfs/blocks/key"
@@ -45,10 +46,6 @@ func NewRoutingPublisher(route routing.IpfsRouting) Publisher {
46
func (p *ipnsPublisher) Publish(ctx context.Context, k ci.PrivKey, value path.Path) error {
47
log.Debugf("Publish %s", value)
48
48
- data, err := createRoutingEntryData(k, value)
49
- if err != nil {
50
- return err
51
- }
49
pubkey := k.GetPublic()
50
pkbytes, err := pubkey.Bytes()
51
if err != nil {
@@ -57,6 +54,27 @@ func (p *ipnsPublisher) Publish(ctx context.Context, k ci.PrivKey, value path.Pa
54
55
nameb := u.Hash(pkbytes)
56
namekey := key.Key("/pk/" + string(nameb))
57
+ ipnskey := key.Key("/ipns/" + string(nameb))
58
+
59
+ // get previous records sequence number, and add one to it
60
+ var seqnum uint64
61
+ prevrec, err := p.routing.GetValues(ctx, ipnskey, 0)
62
+ if err == nil {
63
+ e := new(pb.IpnsEntry)
64
+ err := proto.Unmarshal(prevrec[0].Val, e)
65
+ if err != nil {
66
+ return err
67
+ }
68
+
69
+ seqnum = e.GetSequence() + 1
70
+ } else if err != ds.ErrNotFound {
71
+ return err
72
+ }
73
+
74
+ data, err := createRoutingEntryData(k, value, seqnum)
75
+ if err != nil {
76
+ return err
77
+ }
78
79
log.Debugf("Storing pubkey at: %s", namekey)
80
// Store associated public key
@@ -67,8 +85,6 @@ func (p *ipnsPublisher) Publish(ctx context.Context, k ci.PrivKey, value path.Pa
85
return err
86
}
87
70
- ipnskey := key.Key("/ipns/" + string(nameb))
71
-
88
log.Debugf("Storing ipns entry at: %s", ipnskey)
89
// Store ipns entry at "/ipns/"+b58(h(pubkey))
90
timectx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Second*10))
@@ -80,12 +96,13 @@ func (p *ipnsPublisher) Publish(ctx context.Context, k ci.PrivKey, value path.Pa
96
return nil
97
}
98
83
-func createRoutingEntryData(pk ci.PrivKey, val path.Path) ([]byte, error) {
99
+func createRoutingEntryData(pk ci.PrivKey, val path.Path, seq uint64) ([]byte, error) {
100
entry := new(pb.IpnsEntry)
101
102
entry.Value = []byte(val)
103
typ := pb.IpnsEntry_EOL
104
entry.ValidityType = &typ
105
+ entry.Sequence = proto.Uint64(seq)
106
entry.Validity = []byte(u.FormatRFC3339(time.Now().Add(time.Hour * 24)))
107
108
sig, err := pk.Sign(ipnsEntryDataForSig(entry))
@@ -110,6 +127,52 @@ var IpnsRecordValidator = &record.ValidChecker{
127
Sign: true,
128
}
129
130
+func IpnsSelectorFunc(k key.Key, vals [][]byte) (int, error) {
131
+ var recs []*pb.IpnsEntry
132
+ for _, v := range vals {
133
+ e := new(pb.IpnsEntry)
134
+ err := proto.Unmarshal(v, e)
135
+ if err == nil {
136
+ recs = append(recs, e)
137
+ } else {
138
+ recs = append(recs, nil)
139
+ }
140
+ }
141
+
142
+ var best_seq uint64
143
+ best_i := -1
144
+
145
+ for i, r := range recs {
146
+ if r == nil {
147
+ continue
148
+ }
149
+ if best_i == -1 || r.GetSequence() > best_seq {
150
+ best_seq = r.GetSequence()
151
+ best_i = i
152
+ } else if r.GetSequence() == best_seq {
153
+ rt, err := u.ParseRFC3339(string(r.GetValidity()))
154
+ if err != nil {
155
+ continue
156
+ }
157
+
158
+ bestt, err := u.ParseRFC3339(string(recs[best_i].GetValidity()))
159
+ if err != nil {
160
+ continue
161
+ }
162
+
163
+ if rt.After(bestt) {
164
+ best_seq = r.GetSequence()
165
+ best_i = i
166
+ }
167
+ }
168
+ }
169
+ if best_i == -1 {
170
+ return 0, errors.New("no usable records in given set")
171
+ }
172
+
173
+ return best_i, nil
174
+}
175
+
176
// ValidateIpnsRecord implements ValidatorFunc and verifies that the
177
// given 'val' is an IpnsEntry and that that entry is valid.
178
func ValidateIpnsRecord(k key.Key, val []byte) error {
routing/dht/dht.go
+11
-6
@@ -54,6 +54,7 @@ type IpfsDHT struct {
54
diaglock sync.Mutex // lock to make diagnostics work better
55
56
Validator record.Validator // record validator funcs
57
+ Selector record.Selector // record selection funcs
58
59
ctx context.Context
60
proc goprocess.Process
@@ -89,6 +90,9 @@ func NewDHT(ctx context.Context, h host.Host, dstore ds.ThreadSafeDatastore) *Ip
90
dht.Validator = make(record.Validator)
91
dht.Validator["pk"] = record.PublicKeyValidator
92
93
+ dht.Selector = make(record.Selector)
94
+ dht.Selector["pk"] = record.PublicKeySelector
95
+
96
return dht
97
}
98
@@ -152,13 +156,16 @@ func (dht *IpfsDHT) putProvider(ctx context.Context, p peer.ID, skey string) err
156
// NOTE: it will update the dht's peerstore with any new addresses
157
// it finds for the given peer.
158
func (dht *IpfsDHT) getValueOrPeers(ctx context.Context, p peer.ID,
155
- key key.Key) ([]byte, []peer.PeerInfo, error) {
159
+ key key.Key) (*pb.Record, []peer.PeerInfo, error) {
160
161
pmes, err := dht.getValueSingle(ctx, p, key)
162
if err != nil {
163
return nil, nil, err
164
}
165
166
+ // Perhaps we were given closer peers
167
+ peers := pb.PBPeersToPeerInfos(pmes.GetCloserPeers())
168
+
169
if record := pmes.GetRecord(); record != nil {
170
// Success! We were given the value
171
log.Debug("getValueOrPeers: got value")
@@ -169,11 +176,9 @@ func (dht *IpfsDHT) getValueOrPeers(ctx context.Context, p peer.ID,
176
log.Info("Received invalid record! (discarded)")
177
return nil, nil, err
178
}
172
- return record.GetValue(), nil, nil
179
+ return record, peers, nil
180
}
181
175
- // Perhaps we were given closer peers
176
- peers := pb.PBPeersToPeerInfos(pmes.GetCloserPeers())
182
if len(peers) > 0 {
183
log.Debug("getValueOrPeers: peers")
184
return nil, peers, nil
@@ -193,7 +198,7 @@ func (dht *IpfsDHT) getValueSingle(ctx context.Context, p peer.ID,
198
}
199
200
// getLocal attempts to retrieve the value from the datastore
196
-func (dht *IpfsDHT) getLocal(key key.Key) ([]byte, error) {
201
+func (dht *IpfsDHT) getLocal(key key.Key) (*pb.Record, error) {
202
203
log.Debug("getLocal %s", key)
204
v, err := dht.datastore.Get(key.DsKey())
@@ -221,7 +226,7 @@ func (dht *IpfsDHT) getLocal(key key.Key) ([]byte, error) {
226
}
227
}
228
224
- return rec.GetValue(), nil
229
+ return rec, nil
230
}
231
232
// getOwnPrivateKey attempts to load the local peers private
routing/dht/dht_test.go
+9
-3
@@ -131,8 +131,14 @@ func TestValueGetSet(t *testing.T) {
131
},
132
Sign: false,
133
}
134
+ nulsel := func(_ key.Key, bs [][]byte) (int, error) {
135
+ return 0, nil
136
+ }
137
+
138
dhtA.Validator["v"] = vf
139
dhtB.Validator["v"] = vf
140
+ dhtA.Selector["v"] = nulsel
141
+ dhtB.Selector["v"] = nulsel
142
143
connect(t, ctx, dhtA, dhtB)
144
@@ -193,7 +199,7 @@ func TestProvides(t *testing.T) {
199
if err != nil {
200
t.Fatal(err)
201
}
196
- if !bytes.Equal(bits, v) {
202
+ if !bytes.Equal(bits.GetValue(), v) {
203
t.Fatal("didn't store the right bits (%s, %s)", k, v)
204
}
205
}
@@ -466,7 +472,7 @@ func TestProvidesMany(t *testing.T) {
472
if err != nil {
473
t.Fatal(err)
474
}
469
- if !bytes.Equal(bits, v) {
475
+ if !bytes.Equal(bits.GetValue(), v) {
476
t.Fatal("didn't store the right bits (%s, %s)", k, v)
477
}
478
@@ -558,7 +564,7 @@ func TestProvidesAsync(t *testing.T) {
564
}
565
566
bits, err := dhts[3].getLocal(k)
561
- if err != nil && bytes.Equal(bits, val) {
567
+ if err != nil && bytes.Equal(bits.GetValue(), val) {
568
t.Fatal(err)
569
}
570
routing/dht/handlers.go
+1
-1
@@ -108,7 +108,7 @@ func (dht *IpfsDHT) handlePutValue(ctx context.Context, p peer.ID, pmes *pb.Mess
108
dskey := key.Key(pmes.GetKey()).DsKey()
109
110
if err := dht.verifyRecordLocally(pmes.GetRecord()); err != nil {
111
- log.Debugf("Bad dht record in PUT from: %s. %s", key.Key(pmes.GetRecord().GetAuthor()), err)
111
+ log.Warningf("Bad dht record in PUT from: %s. %s", key.Key(pmes.GetRecord().GetAuthor()), err)
112
return nil, err
113
}
114
routing/dht/routing.go
+84
-18
@@ -1,6 +1,7 @@
1
package dht
2
3
import (
4
+ "bytes"
5
"sync"
6
7
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
@@ -76,16 +77,71 @@ func (dht *IpfsDHT) PutValue(ctx context.Context, key key.Key, value []byte) err
77
}
78
79
// GetValue searches for the value corresponding to given Key.
79
-// If the search does not succeed, a multiaddr string of a closer peer is
80
-// returned along with util.ErrSearchIncomplete
80
func (dht *IpfsDHT) GetValue(ctx context.Context, key key.Key) ([]byte, error) {
81
+ vals, err := dht.GetValues(ctx, key, 3)
82
+ if err != nil {
83
+ return nil, err
84
+ }
85
+
86
+ var recs [][]byte
87
+ for _, v := range vals {
88
+ recs = append(recs, v.Val)
89
+ }
90
+
91
+ i, err := dht.Selector.BestRecord(key, recs)
92
+ if err != nil {
93
+ return nil, err
94
+ }
95
+
96
+ best := recs[i]
97
+ log.Debugf("GetValue %v %v", key, best)
98
+ if best == nil {
99
+ log.Errorf("GetValue yielded correct record with nil value.")
100
+ return nil, routing.ErrNotFound
101
+ }
102
+
103
+ fixupRec, err := record.MakePutRecord(dht.peerstore.PrivKey(dht.self), key, best, true)
104
+ if err != nil {
105
+ // probably shouldnt actually 'error' here as we have found a value we like,
106
+ // but this call failing probably isnt something we want to ignore
107
+ return nil, err
108
+ }
109
+
110
+ for _, v := range vals {
111
+ // if someone sent us a different 'less-valid' record, lets correct them
112
+ if !bytes.Equal(v.Val, best) {
113
+ go func(v routing.RecvdVal) {
114
+ err := dht.putValueToPeer(ctx, v.From, key, fixupRec)
115
+ if err != nil {
116
+ log.Error("Error correcting DHT entry: ", err)
117
+ }
118
+ }(v)
119
+ }
120
+ }
121
+
122
+ return best, nil
123
+}
124
+
125
+func (dht *IpfsDHT) GetValues(ctx context.Context, key key.Key, nvals int) ([]routing.RecvdVal, error) {
126
+ var vals []routing.RecvdVal
127
+ var valslock sync.Mutex
128
+
129
// If we have it local, dont bother doing an RPC!
83
- val, err := dht.getLocal(key)
130
+ lrec, err := dht.getLocal(key)
131
if err == nil {
132
+ // TODO: this is tricky, we dont always want to trust our own value
133
+ // what if the authoritative source updated it?
134
log.Debug("have it locally")
86
- return val, nil
87
- } else {
88
- log.Debug("failed to get value locally: %s", err)
135
+ vals = append(vals, routing.RecvdVal{
136
+ Val: lrec.GetValue(),
137
+ From: dht.self,
138
+ })
139
+
140
+ if nvals <= 1 {
141
+ return vals, nil
142
+ }
143
+ } else if nvals == 0 {
144
+ return nil, err
145
}
146
147
// get closest peers in the routing table
@@ -104,14 +160,26 @@ func (dht *IpfsDHT) GetValue(ctx context.Context, key key.Key) ([]byte, error) {
160
ID: p,
161
})
162
107
- val, peers, err := dht.getValueOrPeers(ctx, p, key)
163
+ rec, peers, err := dht.getValueOrPeers(ctx, p, key)
164
if err != nil {
165
return nil, err
166
}
167
112
- res := &dhtQueryResult{value: val, closerPeers: peers}
113
- if val != nil {
114
- res.success = true
168
+ res := &dhtQueryResult{closerPeers: peers}
169
+
170
+ if rec.GetValue() != nil {
171
+ rv := routing.RecvdVal{
172
+ Val: rec.GetValue(),
173
+ From: p,
174
+ }
175
+ valslock.Lock()
176
+ vals = append(vals, rv)
177
+
178
+ // If weve collected enough records, we're done
179
+ if len(vals) >= nvals {
180
+ res.success = true
181
+ }
182
+ valslock.Unlock()
183
}
184
185
notif.PublishQueryEvent(parent, ¬if.QueryEvent{
@@ -124,17 +192,15 @@ func (dht *IpfsDHT) GetValue(ctx context.Context, key key.Key) ([]byte, error) {
192
})
193
194
// run it!
127
- result, err := query.Run(ctx, rtp)
128
- if err != nil {
129
- return nil, err
195
+ _, err = query.Run(ctx, rtp)
196
+ if len(vals) == 0 {
197
+ if err != nil {
198
+ return nil, err
199
+ }
200
}
201
132
- log.Debugf("GetValue %v %v", key, result.value)
133
- if result.value == nil {
134
- return nil, routing.ErrNotFound
135
- }
202
+ return vals, nil
203
137
- return result.value, nil
204
}
205
206
// Value provider layer of indirection.
routing/mock/centralized_client.go
+15
@@ -44,6 +44,21 @@ func (c *client) GetValue(ctx context.Context, key key.Key) ([]byte, error) {
44
return data, nil
45
}
46
47
+func (c *client) GetValues(ctx context.Context, key key.Key, count int) ([]routing.RecvdVal, error) {
48
+ log.Debugf("GetValue: %s", key)
49
+ v, err := c.datastore.Get(key.DsKey())
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+
54
+ data, ok := v.([]byte)
55
+ if !ok {
56
+ return nil, errors.New("could not cast value from datastore")
57
+ }
58
+
59
+ return []routing.RecvdVal{{Val: data, From: c.peer.ID()}}, nil
60
+}
61
+
62
func (c *client) FindProviders(ctx context.Context, key key.Key) ([]peer.PeerInfo, error) {
63
return c.server.Providers(key), nil
64
}
routing/none/none_client.go
+4
@@ -25,6 +25,10 @@ func (c *nilclient) GetValue(_ context.Context, _ key.Key) ([]byte, error) {
25
return nil, errors.New("Tried GetValue from nil routing.")
26
}
27
28
+func (c *nilclient) GetValues(_ context.Context, _ key.Key, _ int) ([]routing.RecvdVal, error) {
29
+ return nil, errors.New("Tried GetValues from nil routing.")
30
+}
31
+
32
func (c *nilclient) FindPeer(_ context.Context, _ peer.ID) (peer.PeerInfo, error) {
33
return peer.PeerInfo{}, nil
34
}
routing/offline/offline.go
+21
@@ -67,6 +67,27 @@ func (c *offlineRouting) GetValue(ctx context.Context, key key.Key) ([]byte, err
67
return rec.GetValue(), nil
68
}
69
70
+func (c *offlineRouting) GetValues(ctx context.Context, key key.Key, _ int) ([]routing.RecvdVal, error) {
71
+ v, err := c.datastore.Get(key.DsKey())
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+
76
+ byt, ok := v.([]byte)
77
+ if !ok {
78
+ return nil, errors.New("value stored in datastore not []byte")
79
+ }
80
+ rec := new(pb.Record)
81
+ err = proto.Unmarshal(byt, rec)
82
+ if err != nil {
83
+ return nil, err
84
+ }
85
+
86
+ return []routing.RecvdVal{
87
+ {Val: rec.GetValue()},
88
+ }, nil
89
+}
90
+
91
func (c *offlineRouting) FindProviders(ctx context.Context, key key.Key) ([]peer.PeerInfo, error) {
92
return nil, ErrOffline
93
}
routing/record/record.go
-2
@@ -6,13 +6,11 @@ import (
6
proto "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/gogo/protobuf/proto"
7
8
key "github.com/ipfs/go-ipfs/blocks/key"
9
- dag "github.com/ipfs/go-ipfs/merkledag"
9
ci "github.com/ipfs/go-ipfs/p2p/crypto"
10
pb "github.com/ipfs/go-ipfs/routing/dht/pb"
11
logging "github.com/ipfs/go-ipfs/vendor/go-log-v1.0.0"
12
)
13
15
-var _ = dag.FetchGraph
14
var log = logging.Logger("routing/record")
15
16
// MakePutRecord creates and signs a dht record for the given key/value pair
routing/record/selection.go
new
+40
@@ -0,0 +1,40 @@
1
+package record
2
+
3
+import (
4
+ "errors"
5
+ "strings"
6
+
7
+ key "github.com/ipfs/go-ipfs/blocks/key"
8
+)
9
+
10
+// A SelectorFunc selects the best value for the given key from
11
+// a slice of possible values and returns the index of the chosen one
12
+type SelectorFunc func(key.Key, [][]byte) (int, error)
13
+
14
+type Selector map[string]SelectorFunc
15
+
16
+func (s Selector) BestRecord(k key.Key, recs [][]byte) (int, error) {
17
+ if len(recs) == 0 {
18
+ return 0, errors.New("no records given!")
19
+ }
20
+
21
+ parts := strings.Split(string(k), "/")
22
+ if len(parts) < 3 {
23
+ log.Infof("Record key does not have selectorfunc: %s", k)
24
+ return 0, errors.New("record key does not have selectorfunc")
25
+ }
26
+
27
+ sel, ok := s[parts[1]]
28
+ if !ok {
29
+ log.Infof("Unrecognized key prefix: %s", parts[1])
30
+ return 0, ErrInvalidRecordType
31
+ }
32
+
33
+ return sel(k, recs)
34
+}
35
+
36
+// PublicKeySelector just selects the first entry.
37
+// All valid public key records will be equivalent.
38
+func PublicKeySelector(k key.Key, vals [][]byte) (int, error) {
39
+ return 0, nil
40
+}
routing/routing.go
+19
@@ -26,6 +26,18 @@ type IpfsRouting interface {
26
// GetValue searches for the value corresponding to given Key.
27
GetValue(context.Context, key.Key) ([]byte, error)
28
29
+ // GetValues searches for values corresponding to given Key.
30
+ //
31
+ // Passing a value of '0' for the count argument will cause the
32
+ // routing interface to return values only from cached or local storage
33
+ // and return an error if no cached value is found.
34
+ //
35
+ // Passing a value of '1' will return a local value if found, and query
36
+ // the network for the first value it finds otherwise.
37
+ // As a result, a value of '1' is mostly useful for cases where the record
38
+ // in question has only one valid value (such as public keys)
39
+ GetValues(c context.Context, k key.Key, count int) ([]RecvdVal, error)
40
+
41
// Value provider layer of indirection.
42
// This is what DSHTs (Coral and MainlineDHT) do to store large values in a DHT.
43
@@ -44,6 +56,13 @@ type IpfsRouting interface {
56
// TODO expose io.Closer or plain-old Close error
57
}
58
59
+// RecvdVal represents a dht value record that has been received from a given peer
60
+// it is used to track peers with expired records in order to correct them.
61
+type RecvdVal struct {
62
+ From peer.ID
63
+ Val []byte
64
+}
65
+
66
type PubKeyFetcher interface {
67
GetPublicKey(context.Context, peer.ID) (ci.PubKey, error)
68
}
routing/supernode/client.go
+16
@@ -81,6 +81,22 @@ func (c *Client) GetValue(ctx context.Context, k key.Key) ([]byte, error) {
81
return response.Record.GetValue(), nil
82
}
83
84
+func (c *Client) GetValues(ctx context.Context, k key.Key, _ int) ([]routing.RecvdVal, error) {
85
+ defer log.EventBegin(ctx, "getValue", &k).Done()
86
+ msg := pb.NewMessage(pb.Message_GET_VALUE, string(k), 0)
87
+ response, err := c.proxy.SendRequest(ctx, msg) // TODO wrap to hide the remote
88
+ if err != nil {
89
+ return nil, err
90
+ }
91
+
92
+ return []routing.RecvdVal{
93
+ {
94
+ Val: response.Record.GetValue(),
95
+ From: c.local,
96
+ },
97
+ }, nil
98
+}
99
+
100
func (c *Client) Provide(ctx context.Context, k key.Key) error {
101
defer log.EventBegin(ctx, "provide", &k).Done()
102
msg := pb.NewMessage(pb.Message_ADD_PROVIDER, string(k), 0)