extract ipns record logic to go-ipns
License: MIT Signed-off-by: Steven Allen <steven@stebalien.com>
Steven Allen committed
Jun 15, 2018 at 16:53 UTC
dafe495376c28d305950cf0ed14581f1f10ac3ca
15 files changed
+56
-704
Rules.mk
-3
@@ -47,9 +47,6 @@ ifneq ($(filter coverage% clean distclean,$(MAKECMDGOALS)),)
47
include $(dir)/Rules.mk
48
endif
49
50
-dir := namesys/pb
51
-include $(dir)/Rules.mk
52
-
50
dir := unixfs/pb
51
include $(dir)/Rules.mk
52
core/commands/dht_test.go
+6
-3
@@ -4,12 +4,15 @@ import (
4
"testing"
5
6
"github.com/ipfs/go-ipfs/namesys"
7
+
8
tu "gx/ipfs/QmPdxCaVp4jZ9RbxqZADvKH6kiCR5jHvdR5f2ycjAY6T2a/go-testutil"
9
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
10
)
11
12
func TestKeyTranslation(t *testing.T) {
13
pid := tu.RandPeerIDFatal(t)
12
- a, b := namesys.IpnsKeysForID(pid)
14
+ pkname := namesys.PkKeyForID(pid)
15
+ ipnsname := ipns.RecordKey(pid)
16
17
pkk, err := escapeDhtKey("/pk/" + pid.Pretty())
18
if err != nil {
@@ -21,11 +24,11 @@ func TestKeyTranslation(t *testing.T) {
24
t.Fatal(err)
25
}
26
24
- if pkk != a {
27
+ if pkk != pkname {
28
t.Fatal("keys didnt match!")
29
}
30
28
- if ipnsk != b {
31
+ if ipnsk != ipnsname {
32
t.Fatal("keys didnt match!")
33
}
34
}
core/core.go
+2
-1
@@ -42,6 +42,7 @@ import (
42
circuit "gx/ipfs/QmPavh4h3Edx5cv8GJQPC35AQAws7faXmzEZyru7k9b9Mn/go-libp2p-circuit"
43
u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
44
p2phost "gx/ipfs/QmQQGtcp6nVUrQjNsnU53YWV1q8fK1Kd9S7FEkYbRZzxry/go-libp2p-host"
45
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
46
floodsub "gx/ipfs/QmRFEBGcNjtWPupwHA7zGHeGVLuUyE4ZRFi2MgtrPM6pfb/go-libp2p-floodsub"
47
pnet "gx/ipfs/QmRGvSwDpN4eunxgDNfmQhayZ6Z9F5a2v31V2D7y77osLg/go-libp2p-pnet"
48
goprocess "gx/ipfs/QmSF8fPo3jgVBAy8fpdjjYqgG87dkJgUprRBHRd2tmfgpP/goprocess"
@@ -459,7 +460,7 @@ func (n *IpfsNode) startOnlineServicesWithHost(ctx context.Context, host p2phost
460
461
validator := record.NamespacedValidator{
462
"pk": record.PublicKeyValidator{},
462
- "ipns": namesys.IpnsValidator{KeyBook: host.Peerstore()},
463
+ "ipns": ipns.Validator{KeyBook: host.Peerstore()},
464
}
465
466
// setup routing service
namesys/ipns_resolver_validation_test.go
renamed
+8
-154
@@ -2,23 +2,19 @@ package namesys
2
3
import (
4
"context"
5
- "fmt"
6
- "math/rand"
7
- "strings"
5
"testing"
6
"time"
7
8
opts "github.com/ipfs/go-ipfs/namesys/opts"
12
- pb "github.com/ipfs/go-ipfs/namesys/pb"
9
path "github.com/ipfs/go-ipfs/path"
10
11
record "gx/ipfs/QmPWjVzxHeJdrjp4Jr2R2sPxBrMbBgGPWQtKwCKHHCBF7x/go-libp2p-record"
12
u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
13
testutil "gx/ipfs/QmPdxCaVp4jZ9RbxqZADvKH6kiCR5jHvdR5f2ycjAY6T2a/go-testutil"
14
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
15
routing "gx/ipfs/QmUV9hDAAyjeGbxbXkJ2sYqZ6dTd1DXJ2REhYEkRm178Tg/go-libp2p-routing"
16
ropts "gx/ipfs/QmUV9hDAAyjeGbxbXkJ2sYqZ6dTd1DXJ2REhYEkRm178Tg/go-libp2p-routing/options"
17
peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
21
- proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
18
pstore "gx/ipfs/QmZhsmorLpD9kmQ4ynbAu4vbKv2goMUnXazwGA4gnWHDjB/go-libp2p-peerstore"
19
mockrouting "gx/ipfs/Qmb1N7zdjG2FexpzWNj8T289u9QnQLEiSsTRadDGQxX32D/go-ipfs-routing/mock"
20
ci "gx/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5/go-libp2p-crypto"
@@ -26,147 +22,6 @@ import (
22
dssync "gx/ipfs/QmeiCcJfDW1GJnWUArudsv5rQsihpi4oyddPhdqo3CfX6i/go-datastore/sync"
23
)
24
29
-func testValidatorCase(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, exp error) {
30
- t.Helper()
31
-
32
- match := func(t *testing.T, err error) {
33
- t.Helper()
34
- if err != exp {
35
- params := fmt.Sprintf("key: %s\neol: %s\n", key, eol)
36
- if exp == nil {
37
- t.Fatalf("Unexpected error %s for params %s", err, params)
38
- } else if err == nil {
39
- t.Fatalf("Expected error %s but there was no error for params %s", exp, params)
40
- } else {
41
- t.Fatalf("Expected error %s but got %s for params %s", exp, err, params)
42
- }
43
- }
44
- }
45
-
46
- testValidatorCaseMatchFunc(t, priv, kbook, key, val, eol, match)
47
-}
48
-
49
-func testValidatorCaseMatchFunc(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, matchf func(*testing.T, error)) {
50
- t.Helper()
51
- validator := IpnsValidator{kbook}
52
-
53
- data := val
54
- if data == nil {
55
- p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
56
- entry, err := CreateRoutingEntryData(priv, p, 1, eol)
57
- if err != nil {
58
- t.Fatal(err)
59
- }
60
-
61
- data, err = proto.Marshal(entry)
62
- if err != nil {
63
- t.Fatal(err)
64
- }
65
- }
66
-
67
- matchf(t, validator.Validate(key, data))
68
-}
69
-
70
-func TestValidator(t *testing.T) {
71
- ts := time.Now()
72
-
73
- priv, id, _, _ := genKeys(t)
74
- priv2, id2, _, _ := genKeys(t)
75
- kbook := pstore.NewPeerstore()
76
- kbook.AddPubKey(id, priv.GetPublic())
77
- emptyKbook := pstore.NewPeerstore()
78
-
79
- testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), nil)
80
- testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour*-1), ErrExpiredRecord)
81
- testValidatorCase(t, priv, kbook, "/ipns/"+string(id), []byte("bad data"), ts.Add(time.Hour), ErrBadRecord)
82
- testValidatorCase(t, priv, kbook, "/ipns/"+"bad key", nil, ts.Add(time.Hour), ErrKeyFormat)
83
- testValidatorCase(t, priv, emptyKbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrPublicKeyNotFound)
84
- testValidatorCase(t, priv2, kbook, "/ipns/"+string(id2), nil, ts.Add(time.Hour), ErrPublicKeyNotFound)
85
- testValidatorCase(t, priv2, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrSignature)
86
- testValidatorCase(t, priv, kbook, "//"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
87
- testValidatorCase(t, priv, kbook, "/wrong/"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
88
-}
89
-
90
-func mustMarshal(t *testing.T, entry *pb.IpnsEntry) []byte {
91
- t.Helper()
92
- data, err := proto.Marshal(entry)
93
- if err != nil {
94
- t.Fatal(err)
95
- }
96
- return data
97
-}
98
-
99
-func TestEmbeddedPubKeyValidate(t *testing.T) {
100
- goodeol := time.Now().Add(time.Hour)
101
- kbook := pstore.NewPeerstore()
102
-
103
- pth := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
104
-
105
- priv, _, _, ipnsk := genKeys(t)
106
-
107
- entry, err := CreateRoutingEntryData(priv, pth, 1, goodeol)
108
- if err != nil {
109
- t.Fatal(err)
110
- }
111
-
112
- testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyNotFound)
113
-
114
- pubkb, err := priv.GetPublic().Bytes()
115
- if err != nil {
116
- t.Fatal(err)
117
- }
118
-
119
- entry.PubKey = pubkb
120
- testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, nil)
121
-
122
- entry.PubKey = []byte("probably not a public key")
123
- testValidatorCaseMatchFunc(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, func(t *testing.T, err error) {
124
- if !strings.Contains(err.Error(), "unmarshaling pubkey in record:") {
125
- t.Fatal("expected pubkey unmarshaling to fail")
126
- }
127
- })
128
-
129
- opriv, _, _, _ := genKeys(t)
130
- wrongkeydata, err := opriv.GetPublic().Bytes()
131
- if err != nil {
132
- t.Fatal(err)
133
- }
134
-
135
- entry.PubKey = wrongkeydata
136
- testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyMismatch)
137
-}
138
-
139
-func TestPeerIDPubKeyValidate(t *testing.T) {
140
- goodeol := time.Now().Add(time.Hour)
141
- kbook := pstore.NewPeerstore()
142
-
143
- pth := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
144
-
145
- sk, pk, err := ci.GenerateEd25519Key(rand.New(rand.NewSource(42)))
146
- if err != nil {
147
- t.Fatal(err)
148
- }
149
-
150
- pid, err := peer.IDFromPublicKey(pk)
151
- if err != nil {
152
- t.Fatal(err)
153
- }
154
-
155
- ipnsk := "/ipns/" + string(pid)
156
-
157
- entry, err := CreateRoutingEntryData(sk, pth, 1, goodeol)
158
- if err != nil {
159
- t.Fatal(err)
160
- }
161
-
162
- dataNoKey, err := proto.Marshal(entry)
163
- if err != nil {
164
- t.Fatal(err)
165
- }
166
-
167
- testValidatorCase(t, sk, kbook, ipnsk, dataNoKey, goodeol, nil)
168
-}
169
-
25
func TestResolverValidation(t *testing.T) {
26
ctx := context.Background()
27
rid := testutil.RandIdentityOrFatal(t)
@@ -179,8 +34,8 @@ func TestResolverValidation(t *testing.T) {
34
// Create entry with expiry in one hour
35
priv, id, _, ipnsDHTPath := genKeys(t)
36
ts := time.Now()
182
- p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
183
- entry, err := CreateRoutingEntryData(priv, p, 1, ts.Add(time.Hour))
37
+ p := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
38
+ entry, err := ipns.Create(priv, p, 1, ts.Add(time.Hour))
39
if err != nil {
40
t.Fatal(err)
41
}
@@ -202,12 +57,12 @@ func TestResolverValidation(t *testing.T) {
57
if err != nil {
58
t.Fatal(err)
59
}
205
- if resp != p {
60
+ if resp != path.Path(p) {
61
t.Fatalf("Mismatch between published path %s and resolved path %s", p, resp)
62
}
63
64
// Create expired entry
210
- expiredEntry, err := CreateRoutingEntryData(priv, p, 1, ts.Add(-1*time.Hour))
65
+ expiredEntry, err := ipns.Create(priv, p, 1, ts.Add(-1*time.Hour))
66
if err != nil {
67
t.Fatal(err)
68
}
@@ -248,7 +103,7 @@ func TestResolverValidation(t *testing.T) {
103
104
// Publish entry without making public key available in peer store
105
priv3, id3, pubkDHTPath3, ipnsDHTPath3 := genKeys(t)
251
- entry3, err := CreateRoutingEntryData(priv3, p, 1, ts.Add(time.Hour))
106
+ entry3, err := ipns.Create(priv3, p, 1, ts.Add(time.Hour))
107
if err != nil {
108
t.Fatal(err)
109
}
@@ -292,9 +147,8 @@ func genKeys(t *testing.T) (ci.PrivKey, peer.ID, string, string) {
147
if err != nil {
148
t.Fatal(err)
149
}
295
- pubkDHTPath, ipnsDHTPath := IpnsKeysForID(pid)
150
297
- return priv, pid, pubkDHTPath, ipnsDHTPath
151
+ return priv, pid, PkKeyForID(pid), ipns.RecordKey(pid)
152
}
153
154
type mockValueStore struct {
@@ -307,7 +161,7 @@ func newMockValueStore(id testutil.Identity, dstore ds.Datastore, kbook pstore.K
161
serv := mockrouting.NewServer()
162
r := serv.ClientWithDatastore(context.Background(), id, dstore)
163
return &mockValueStore{r, kbook, record.NamespacedValidator{
310
- "ipns": IpnsValidator{kbook},
164
+ "ipns": ipns.Validator{KeyBook: kbook},
165
"pk": record.PublicKeyValidator{},
166
}}
167
}
namesys/ipns_select_test.go
deleted
-127
@@ -1,127 +0,0 @@
1
-package namesys
2
-
3
-import (
4
- "fmt"
5
- "math/rand"
6
- "testing"
7
- "time"
8
-
9
- pb "github.com/ipfs/go-ipfs/namesys/pb"
10
- path "github.com/ipfs/go-ipfs/path"
11
-
12
- u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
13
- proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
14
- ci "gx/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5/go-libp2p-crypto"
15
-)
16
-
17
-func shuffle(a []*pb.IpnsEntry) {
18
- for n := 0; n < 5; n++ {
19
- for i, _ := range a {
20
- j := rand.Intn(len(a))
21
- a[i], a[j] = a[j], a[i]
22
- }
23
- }
24
-}
25
-
26
-func AssertSelected(r *pb.IpnsEntry, from ...*pb.IpnsEntry) error {
27
- shuffle(from)
28
- var vals [][]byte
29
- for _, r := range from {
30
- data, err := proto.Marshal(r)
31
- if err != nil {
32
- return err
33
- }
34
- vals = append(vals, data)
35
- }
36
-
37
- i, err := selectRecord(from, vals)
38
- if err != nil {
39
- return err
40
- }
41
-
42
- if from[i] != r {
43
- return fmt.Errorf("selected incorrect record %d", i)
44
- }
45
-
46
- return nil
47
-}
48
-
49
-func TestOrdering(t *testing.T) {
50
- // select timestamp so selection is deterministic
51
- ts := time.Unix(1000000, 0)
52
-
53
- // generate a key for signing the records
54
- r := u.NewSeededRand(15) // generate deterministic keypair
55
- priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, r)
56
- if err != nil {
57
- t.Fatal(err)
58
- }
59
-
60
- e1, err := CreateRoutingEntryData(priv, path.Path("foo"), 1, ts.Add(time.Hour))
61
- if err != nil {
62
- t.Fatal(err)
63
- }
64
-
65
- e2, err := CreateRoutingEntryData(priv, path.Path("bar"), 2, ts.Add(time.Hour))
66
- if err != nil {
67
- t.Fatal(err)
68
- }
69
-
70
- e3, err := CreateRoutingEntryData(priv, path.Path("baz"), 3, ts.Add(time.Hour))
71
- if err != nil {
72
- t.Fatal(err)
73
- }
74
-
75
- e4, err := CreateRoutingEntryData(priv, path.Path("cat"), 3, ts.Add(time.Hour*2))
76
- if err != nil {
77
- t.Fatal(err)
78
- }
79
-
80
- e5, err := CreateRoutingEntryData(priv, path.Path("dog"), 4, ts.Add(time.Hour*3))
81
- if err != nil {
82
- t.Fatal(err)
83
- }
84
-
85
- e6, err := CreateRoutingEntryData(priv, path.Path("fish"), 4, ts.Add(time.Hour*3))
86
- if err != nil {
87
- t.Fatal(err)
88
- }
89
-
90
- // e1 is the only record, i hope it gets this right
91
- err = AssertSelected(e1, e1)
92
- if err != nil {
93
- t.Fatal(err)
94
- }
95
-
96
- // e2 has the highest sequence number
97
- err = AssertSelected(e2, e1, e2)
98
- if err != nil {
99
- t.Fatal(err)
100
- }
101
-
102
- // e3 has the highest sequence number
103
- err = AssertSelected(e3, e1, e2, e3)
104
- if err != nil {
105
- t.Fatal(err)
106
- }
107
-
108
- // e4 has a higher timeout
109
- err = AssertSelected(e4, e1, e2, e3, e4)
110
- if err != nil {
111
- t.Fatal(err)
112
- }
113
-
114
- // e5 has the highest sequence number
115
- err = AssertSelected(e5, e1, e2, e3, e4, e5)
116
- if err != nil {
117
- t.Fatal(err)
118
- }
119
-
120
- // e6 should be selected as its signauture will win in the comparison
121
- err = AssertSelected(e6, e1, e2, e3, e4, e5, e6)
122
- if err != nil {
123
- t.Fatal(err)
124
- }
125
-
126
- _ = []interface{}{e1, e2, e3, e4, e5, e6}
127
-}
namesys/pb/Rules.mk
deleted
-8
@@ -1,8 +0,0 @@
1
-include mk/header.mk
2
-
3
-PB_$(d) = $(wildcard $(d)/*.proto)
4
-TGTS_$(d) = $(PB_$(d):.proto=.pb.go)
5
-
6
-#DEPS_GO += $(TGTS_$(d))
7
-
8
-include mk/footer.mk
namesys/pb/namesys.pb.go
deleted
-127
@@ -1,127 +0,0 @@
1
-// Code generated by protoc-gen-gogo.
2
-// source: namesys/pb/namesys.proto
3
-// DO NOT EDIT!
4
-
5
-/*
6
-Package namesys_pb is a generated protocol buffer package.
7
-
8
-It is generated from these files:
9
- namesys/pb/namesys.proto
10
-
11
-It has these top-level messages:
12
- IpnsEntry
13
-*/
14
-package namesys_pb
15
-
16
-import proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
17
-import fmt "fmt"
18
-import math "math"
19
-
20
-// Reference imports to suppress errors if they are not otherwise used.
21
-var _ = proto.Marshal
22
-var _ = fmt.Errorf
23
-var _ = math.Inf
24
-
25
-type IpnsEntry_ValidityType int32
26
-
27
-const (
28
- // setting an EOL says "this record is valid until..."
29
- IpnsEntry_EOL IpnsEntry_ValidityType = 0
30
-)
31
-
32
-var IpnsEntry_ValidityType_name = map[int32]string{
33
- 0: "EOL",
34
-}
35
-var IpnsEntry_ValidityType_value = map[string]int32{
36
- "EOL": 0,
37
-}
38
-
39
-func (x IpnsEntry_ValidityType) Enum() *IpnsEntry_ValidityType {
40
- p := new(IpnsEntry_ValidityType)
41
- *p = x
42
- return p
43
-}
44
-func (x IpnsEntry_ValidityType) String() string {
45
- return proto.EnumName(IpnsEntry_ValidityType_name, int32(x))
46
-}
47
-func (x *IpnsEntry_ValidityType) UnmarshalJSON(data []byte) error {
48
- value, err := proto.UnmarshalJSONEnum(IpnsEntry_ValidityType_value, data, "IpnsEntry_ValidityType")
49
- if err != nil {
50
- return err
51
- }
52
- *x = IpnsEntry_ValidityType(value)
53
- return nil
54
-}
55
-
56
-type IpnsEntry struct {
57
- Value []byte `protobuf:"bytes,1,req,name=value" json:"value,omitempty"`
58
- Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"`
59
- ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=namesys.pb.IpnsEntry_ValidityType" json:"validityType,omitempty"`
60
- Validity []byte `protobuf:"bytes,4,opt,name=validity" json:"validity,omitempty"`
61
- Sequence *uint64 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"`
62
- Ttl *uint64 `protobuf:"varint,6,opt,name=ttl" json:"ttl,omitempty"`
63
- // in order for nodes to properly validate a record upon receipt, they need the public
64
- // key associated with it. For old RSA keys, its easiest if we just send this as part of
65
- // the record itself. For newer ed25519 keys, the public key can be embedded in the
66
- // peerID, making this field unnecessary.
67
- PubKey []byte `protobuf:"bytes,7,opt,name=pubKey" json:"pubKey,omitempty"`
68
- XXX_unrecognized []byte `json:"-"`
69
-}
70
-
71
-func (m *IpnsEntry) Reset() { *m = IpnsEntry{} }
72
-func (m *IpnsEntry) String() string { return proto.CompactTextString(m) }
73
-func (*IpnsEntry) ProtoMessage() {}
74
-
75
-func (m *IpnsEntry) GetValue() []byte {
76
- if m != nil {
77
- return m.Value
78
- }
79
- return nil
80
-}
81
-
82
-func (m *IpnsEntry) GetSignature() []byte {
83
- if m != nil {
84
- return m.Signature
85
- }
86
- return nil
87
-}
88
-
89
-func (m *IpnsEntry) GetValidityType() IpnsEntry_ValidityType {
90
- if m != nil && m.ValidityType != nil {
91
- return *m.ValidityType
92
- }
93
- return IpnsEntry_EOL
94
-}
95
-
96
-func (m *IpnsEntry) GetValidity() []byte {
97
- if m != nil {
98
- return m.Validity
99
- }
100
- return nil
101
-}
102
-
103
-func (m *IpnsEntry) GetSequence() uint64 {
104
- if m != nil && m.Sequence != nil {
105
- return *m.Sequence
106
- }
107
- return 0
108
-}
109
-
110
-func (m *IpnsEntry) GetTtl() uint64 {
111
- if m != nil && m.Ttl != nil {
112
- return *m.Ttl
113
- }
114
- return 0
115
-}
116
-
117
-func (m *IpnsEntry) GetPubKey() []byte {
118
- if m != nil {
119
- return m.PubKey
120
- }
121
- return nil
122
-}
123
-
124
-func init() {
125
- proto.RegisterType((*IpnsEntry)(nil), "namesys.pb.IpnsEntry")
126
- proto.RegisterEnum("namesys.pb.IpnsEntry_ValidityType", IpnsEntry_ValidityType_name, IpnsEntry_ValidityType_value)
127
-}
namesys/pb/namesys.proto
deleted
-23
@@ -1,23 +0,0 @@
1
-package namesys.pb;
2
-
3
-message IpnsEntry {
4
- enum ValidityType {
5
- // setting an EOL says "this record is valid until..."
6
- EOL = 0;
7
- }
8
- required bytes value = 1;
9
- required bytes signature = 2;
10
-
11
- optional ValidityType validityType = 3;
12
- optional bytes validity = 4;
13
-
14
- optional uint64 sequence = 5;
15
-
16
- optional uint64 ttl = 6;
17
-
18
- // in order for nodes to properly validate a record upon receipt, they need the public
19
- // key associated with it. For old RSA keys, its easiest if we just send this as part of
20
- // the record itself. For newer ed25519 keys, the public key can be embedded in the
21
- // peerID, making this field unnecessary.
22
- optional bytes pubKey = 7;
23
-}
namesys/publisher.go
+16
-56
@@ -1,19 +1,18 @@
1
package namesys
2
3
import (
4
- "bytes"
4
"context"
5
"fmt"
6
"strings"
7
"sync"
8
"time"
9
11
- pb "github.com/ipfs/go-ipfs/namesys/pb"
10
path "github.com/ipfs/go-ipfs/path"
11
pin "github.com/ipfs/go-ipfs/pin"
12
ft "github.com/ipfs/go-ipfs/unixfs"
13
16
- u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
14
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
15
+ pb "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns/pb"
16
routing "gx/ipfs/QmUV9hDAAyjeGbxbXkJ2sYqZ6dTd1DXJ2REhYEkRm178Tg/go-libp2p-routing"
17
peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
18
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
@@ -131,7 +130,7 @@ func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouti
130
if !checkRouting {
131
return nil, nil
132
}
134
- _, ipnskey := IpnsKeysForID(id)
133
+ ipnskey := ipns.RecordKey(id)
134
value, err = p.routing.GetValue(ctx, ipnskey)
135
if err != nil {
136
// Not found or other network issue. Can't really do
@@ -175,7 +174,7 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k ci.PrivKey, value pa
174
}
175
176
// Create record
178
- entry, err := CreateRoutingEntryData(k, value, seqno, eol)
177
+ entry, err := ipns.Create(k, []byte(value), seqno, eol)
178
if err != nil {
179
return nil, err
180
}
@@ -229,40 +228,29 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k ci.PubKey,
228
229
errs := make(chan error, 2) // At most two errors (IPNS, and public key)
230
232
- id, err := peer.IDFromPublicKey(k)
233
- if err != nil {
231
+ if err := ipns.EmbedPublicKey(k, entry); err != nil {
232
return err
233
}
234
237
- // Attempt to extract the public key from the ID
238
- extractedPublicKey, err := id.ExtractPublicKey()
235
+ id, err := peer.IDFromPublicKey(k)
236
if err != nil {
237
return err
238
}
239
243
- // if we can't derive the public key from the peerID, embed the entire pubkey in
244
- // the record to make the verifiers job easier
245
- if extractedPublicKey == nil {
246
- pubkeyBytes, err := k.Bytes()
247
- if err != nil {
248
- return err
249
- }
250
-
251
- entry.PubKey = pubkeyBytes
252
- }
253
-
254
- namekey, ipnskey := IpnsKeysForID(id)
255
-
240
go func() {
257
- errs <- PublishEntry(ctx, r, ipnskey, entry)
241
+ errs <- PublishEntry(ctx, r, ipns.RecordKey(id), entry)
242
}()
243
244
// Publish the public key if a public key cannot be extracted from the ID
245
// TODO: once v0.4.16 is widespread enough, we can stop doing this
246
// and at that point we can even deprecate the /pk/ namespace in the dht
263
- if extractedPublicKey == nil {
247
+ //
248
+ // NOTE: This check actually checks if the public key has been embedded
249
+ // in the IPNS entry. This check is sufficient because we embed the
250
+ // public key in the IPNS entry if it can't be extracted from the ID.
251
+ if entry.PubKey != nil {
252
go func() {
265
- errs <- PublishPublicKey(ctx, r, namekey, k)
253
+ errs <- PublishPublicKey(ctx, r, PkKeyForID(id), k)
254
}()
255
256
if err := waitOnErrChan(ctx, errs); err != nil {
@@ -309,32 +297,6 @@ func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec
297
return r.PutValue(timectx, ipnskey, data)
298
}
299
312
-func CreateRoutingEntryData(pk ci.PrivKey, val path.Path, seq uint64, eol time.Time) (*pb.IpnsEntry, error) {
313
- entry := new(pb.IpnsEntry)
314
-
315
- entry.Value = []byte(val)
316
- typ := pb.IpnsEntry_EOL
317
- entry.ValidityType = &typ
318
- entry.Sequence = proto.Uint64(seq)
319
- entry.Validity = []byte(u.FormatRFC3339(eol))
320
-
321
- sig, err := pk.Sign(ipnsEntryDataForSig(entry))
322
- if err != nil {
323
- return nil, err
324
- }
325
- entry.Signature = sig
326
- return entry, nil
327
-}
328
-
329
-func ipnsEntryDataForSig(e *pb.IpnsEntry) []byte {
330
- return bytes.Join([][]byte{
331
- e.Value,
332
- e.Validity,
333
- []byte(fmt.Sprint(e.GetValidityType())),
334
- },
335
- []byte{})
336
-}
337
-
300
// InitializeKeyspace sets the ipns record for the given key to
301
// point to an empty directory.
302
// TODO: this doesnt feel like it belongs here
@@ -356,9 +318,7 @@ func InitializeKeyspace(ctx context.Context, pub Publisher, pins pin.Pinner, key
318
return pub.Publish(ctx, key, path.FromCid(emptyDir.Cid()))
319
}
320
359
-func IpnsKeysForID(id peer.ID) (name, ipns string) {
360
- namekey := "/pk/" + string(id)
361
- ipnskey := "/ipns/" + string(id)
362
-
363
- return namekey, ipnskey
321
+// PkKeyForID returns the public key routing key for the given peer ID.
322
+func PkKeyForID(id peer.ID) string {
323
+ return "/pk/" + string(id)
324
}
namesys/publisher_test.go
+4
-5
@@ -6,10 +6,9 @@ import (
6
"testing"
7
"time"
8
9
- path "github.com/ipfs/go-ipfs/path"
10
-
9
dshelp "gx/ipfs/QmNP2u7bofwUQptHQGPfabGWtTCbxhNLSZKqbf1uzsup9V/go-ipfs-ds-help"
10
testutil "gx/ipfs/QmPdxCaVp4jZ9RbxqZADvKH6kiCR5jHvdR5f2ycjAY6T2a/go-testutil"
11
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
12
ma "gx/ipfs/QmUxSEGbv2nmYNnfXi7839wwQqTN3kwQeUxe8dTjZWZs7J/go-multiaddr"
13
peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
14
mockrouting "gx/ipfs/Qmb1N7zdjG2FexpzWNj8T289u9QnQLEiSsTRadDGQxX32D/go-ipfs-routing/mock"
@@ -55,7 +54,7 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected
54
}
55
56
// Value
58
- value := path.Path("ipfs/TESTING")
57
+ value := []byte("ipfs/TESTING")
58
59
// Seqnum
60
seqnum := uint64(0)
@@ -75,7 +74,7 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected
74
serv := mockrouting.NewServer()
75
r := serv.ClientWithDatastore(context.Background(), &identity{p}, dstore)
76
78
- entry, err := CreateRoutingEntryData(privKey, value, seqnum, eol)
77
+ entry, err := ipns.Create(privKey, value, seqnum, eol)
78
if err != nil {
79
t.Fatal(err)
80
}
@@ -86,7 +85,7 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected
85
}
86
87
// Check for namekey existence in value store
89
- namekey, _ := IpnsKeysForID(id)
88
+ namekey := PkKeyForID(id)
89
_, err = r.GetValue(ctx, namekey)
90
if err != expectedErr {
91
t.Fatal(err)
namesys/republisher/repub.go
+1
-1
@@ -7,9 +7,9 @@ import (
7
8
keystore "github.com/ipfs/go-ipfs/keystore"
9
namesys "github.com/ipfs/go-ipfs/namesys"
10
- pb "github.com/ipfs/go-ipfs/namesys/pb"
10
path "github.com/ipfs/go-ipfs/path"
11
12
+ pb "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns/pb"
13
goprocess "gx/ipfs/QmSF8fPo3jgVBAy8fpdjjYqgG87dkJgUprRBHRd2tmfgpP/goprocess"
14
gpctx "gx/ipfs/QmSF8fPo3jgVBAy8fpdjjYqgG87dkJgUprRBHRd2tmfgpP/goprocess/context"
15
peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
namesys/resolve_test.go
+3
-2
@@ -9,6 +9,7 @@ import (
9
path "github.com/ipfs/go-ipfs/path"
10
11
testutil "gx/ipfs/QmPdxCaVp4jZ9RbxqZADvKH6kiCR5jHvdR5f2ycjAY6T2a/go-testutil"
12
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
13
peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
14
mockrouting "gx/ipfs/Qmb1N7zdjG2FexpzWNj8T289u9QnQLEiSsTRadDGQxX32D/go-ipfs-routing/mock"
15
ds "gx/ipfs/QmeiCcJfDW1GJnWUArudsv5rQsihpi4oyddPhdqo3CfX6i/go-datastore"
@@ -71,7 +72,7 @@ func TestPrexistingExpiredRecord(t *testing.T) {
72
h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN")
73
eol := time.Now().Add(time.Hour * -1)
74
74
- entry, err := CreateRoutingEntryData(privk, h, 0, eol)
75
+ entry, err := ipns.Create(privk, []byte(h), 0, eol)
76
if err != nil {
77
t.Fatal(err)
78
}
@@ -112,7 +113,7 @@ func TestPrexistingRecord(t *testing.T) {
113
// Make a good record and put it in the datastore
114
h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN")
115
eol := time.Now().Add(time.Hour)
115
- entry, err := CreateRoutingEntryData(privk, h, 0, eol)
116
+ entry, err := ipns.Create(privk, []byte(h), 0, eol)
117
if err != nil {
118
t.Fatal(err)
119
}
namesys/routing.go
+10
-15
@@ -6,11 +6,11 @@ import (
6
"time"
7
8
opts "github.com/ipfs/go-ipfs/namesys/opts"
9
- pb "github.com/ipfs/go-ipfs/namesys/pb"
9
path "github.com/ipfs/go-ipfs/path"
10
12
- u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
11
mh "gx/ipfs/QmPnFwZ2JXKnXgMw8CdBPxn7FWh6LLdjUjxV1fKHuJnkr8/go-multihash"
12
+ ipns "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns"
13
+ pb "gx/ipfs/QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt/go-ipns/pb"
14
routing "gx/ipfs/QmUV9hDAAyjeGbxbXkJ2sYqZ6dTd1DXJ2REhYEkRm178Tg/go-libp2p-routing"
15
peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
16
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
@@ -82,7 +82,7 @@ func (r *IpnsResolver) resolveOnce(ctx context.Context, name string, options *op
82
// Use the routing system to get the name.
83
// Note that the DHT will call the ipns validator when retrieving
84
// the value, which in turn verifies the ipns record signature
85
- _, ipnsKey := IpnsKeysForID(pid)
85
+ ipnsKey := ipns.RecordKey(pid)
86
val, err := r.routing.GetValue(ctx, ipnsKey, dht.Quorum(int(options.DhtRecordCount)))
87
if err != nil {
88
log.Debugf("RoutingResolver: dht get for name %s failed: %s", name, err)
@@ -114,7 +114,10 @@ func (r *IpnsResolver) resolveOnce(ctx context.Context, name string, options *op
114
if entry.Ttl != nil {
115
ttl = time.Duration(*entry.Ttl)
116
}
117
- if eol, ok := checkEOL(entry); ok {
117
+ switch eol, err := ipns.GetEOL(entry); err {
118
+ case ipns.ErrUnrecognizedValidity:
119
+ // No EOL.
120
+ case nil:
121
ttEol := eol.Sub(time.Now())
122
if ttEol < 0 {
123
// It *was* valid when we first resolved it.
@@ -122,18 +125,10 @@ func (r *IpnsResolver) resolveOnce(ctx context.Context, name string, options *op
125
} else if ttEol < ttl {
126
ttl = ttEol
127
}
128
+ default:
129
+ log.Errorf("encountered error when parsing EOL: %s", err)
130
+ return "", 0, err
131
}
132
133
return p, ttl, nil
134
}
129
-
130
-func checkEOL(e *pb.IpnsEntry) (time.Time, bool) {
131
- if e.GetValidityType() == pb.IpnsEntry_EOL {
132
- eol, err := u.ParseRFC3339(string(e.GetValidity()))
133
- if err != nil {
134
- return time.Time{}, false
135
- }
136
- return eol, true
137
- }
138
- return time.Time{}, false
139
-}
namesys/validator.go
deleted
-179
@@ -1,179 +0,0 @@
1
-package namesys
2
-
3
-import (
4
- "bytes"
5
- "errors"
6
- "fmt"
7
- "time"
8
-
9
- pb "github.com/ipfs/go-ipfs/namesys/pb"
10
- peer "gx/ipfs/QmVf8hTAsLLFtn4WPCRNdnaF2Eag2qTBS6uR8AiHPZARXy/go-libp2p-peer"
11
- pstore "gx/ipfs/QmZhsmorLpD9kmQ4ynbAu4vbKv2goMUnXazwGA4gnWHDjB/go-libp2p-peerstore"
12
- ic "gx/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5/go-libp2p-crypto"
13
-
14
- record "gx/ipfs/QmPWjVzxHeJdrjp4Jr2R2sPxBrMbBgGPWQtKwCKHHCBF7x/go-libp2p-record"
15
- u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
16
- proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
17
-)
18
-
19
-// ErrExpiredRecord should be returned when an ipns record is
20
-// invalid due to being too old
21
-var ErrExpiredRecord = errors.New("expired record")
22
-
23
-// ErrUnrecognizedValidity is returned when an IpnsRecord has an
24
-// unknown validity type.
25
-var ErrUnrecognizedValidity = errors.New("unrecognized validity type")
26
-
27
-// ErrInvalidPath should be returned when an ipns record path
28
-// is not in a valid format
29
-var ErrInvalidPath = errors.New("record path invalid")
30
-
31
-// ErrSignature should be returned when an ipns record fails
32
-// signature verification
33
-var ErrSignature = errors.New("record signature verification failed")
34
-
35
-// ErrBadRecord should be returned when an ipns record cannot be unmarshalled
36
-var ErrBadRecord = errors.New("record could not be unmarshalled")
37
-
38
-// ErrKeyFormat should be returned when an ipns record key is
39
-// incorrectly formatted (not a peer ID)
40
-var ErrKeyFormat = errors.New("record key could not be parsed into peer ID")
41
-
42
-// ErrPublicKeyNotFound should be returned when the public key
43
-// corresponding to the ipns record path cannot be retrieved
44
-// from the peer store
45
-var ErrPublicKeyNotFound = errors.New("public key not found in peer store")
46
-
47
-var ErrPublicKeyMismatch = errors.New("public key in record did not match expected pubkey")
48
-
49
-type IpnsValidator struct {
50
- KeyBook pstore.KeyBook
51
-}
52
-
53
-func (v IpnsValidator) Validate(key string, value []byte) error {
54
- ns, pidString, err := record.SplitKey(key)
55
- if err != nil || ns != "ipns" {
56
- return ErrInvalidPath
57
- }
58
-
59
- // Parse the value into an IpnsEntry
60
- entry := new(pb.IpnsEntry)
61
- err = proto.Unmarshal(value, entry)
62
- if err != nil {
63
- return ErrBadRecord
64
- }
65
-
66
- // Get the public key defined by the ipns path
67
- pid, err := peer.IDFromString(pidString)
68
- if err != nil {
69
- log.Debugf("failed to parse ipns record key %s into peer ID", pidString)
70
- return ErrKeyFormat
71
- }
72
-
73
- pubk, err := v.getPublicKey(pid, entry)
74
- if err != nil {
75
- return err
76
- }
77
-
78
- // Check the ipns record signature with the public key
79
- if ok, err := pubk.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
80
- log.Debugf("failed to verify signature for ipns record %s", pidString)
81
- return ErrSignature
82
- }
83
-
84
- // Check that record has not expired
85
- switch entry.GetValidityType() {
86
- case pb.IpnsEntry_EOL:
87
- t, err := u.ParseRFC3339(string(entry.GetValidity()))
88
- if err != nil {
89
- log.Debugf("failed parsing time for ipns record EOL in record %s", pidString)
90
- return err
91
- }
92
- if time.Now().After(t) {
93
- return ErrExpiredRecord
94
- }
95
- default:
96
- return ErrUnrecognizedValidity
97
- }
98
- return nil
99
-}
100
-
101
-func (v IpnsValidator) getPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
102
- if entry.PubKey != nil {
103
- pk, err := ic.UnmarshalPublicKey(entry.PubKey)
104
- if err != nil {
105
- log.Debugf("public key in ipns record failed to parse: ", err)
106
- return nil, fmt.Errorf("unmarshaling pubkey in record: %s", err)
107
- }
108
-
109
- expPid, err := peer.IDFromPublicKey(pk)
110
- if err != nil {
111
- return nil, fmt.Errorf("could not regenerate peerID from pubkey: %s", err)
112
- }
113
-
114
- if pid != expPid {
115
- return nil, ErrPublicKeyMismatch
116
- }
117
-
118
- return pk, nil
119
- }
120
-
121
- pubk := v.KeyBook.PubKey(pid)
122
- if pubk == nil {
123
- log.Debugf("public key with hash %s not found in peer store", pid)
124
- return nil, ErrPublicKeyNotFound
125
- }
126
- return pubk, nil
127
-}
128
-
129
-// IpnsSelectorFunc selects the best record by checking which has the highest
130
-// sequence number and latest EOL
131
-func (v IpnsValidator) Select(k string, vals [][]byte) (int, error) {
132
- var recs []*pb.IpnsEntry
133
- for _, v := range vals {
134
- e := new(pb.IpnsEntry)
135
- err := proto.Unmarshal(v, e)
136
- if err == nil {
137
- recs = append(recs, e)
138
- } else {
139
- recs = append(recs, nil)
140
- }
141
- }
142
-
143
- return selectRecord(recs, vals)
144
-}
145
-
146
-func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) {
147
- var bestSeq uint64
148
- besti := -1
149
-
150
- for i, r := range recs {
151
- if r == nil || r.GetSequence() < bestSeq {
152
- continue
153
- }
154
- rt, err := u.ParseRFC3339(string(r.GetValidity()))
155
- if err != nil {
156
- log.Errorf("failed to parse ipns record EOL %s", r.GetValidity())
157
- continue
158
- }
159
-
160
- if besti == -1 || r.GetSequence() > bestSeq {
161
- bestSeq = r.GetSequence()
162
- besti = i
163
- } else if r.GetSequence() == bestSeq {
164
- bestt, _ := u.ParseRFC3339(string(recs[besti].GetValidity()))
165
- if rt.After(bestt) {
166
- besti = i
167
- } else if rt == bestt {
168
- if bytes.Compare(vals[i], vals[besti]) > 0 {
169
- besti = i
170
- }
171
- }
172
- }
173
- }
174
- if besti == -1 {
175
- return 0, errors.New("no usable records in given set")
176
- }
177
-
178
- return besti, nil
179
-}
package.json
+6
@@ -539,6 +539,12 @@
539
"hash": "QmfNjggF4Pt6erqg3NDafD3MdvDHk1qqCVr8pL5hnPucS8",
540
"name": "fsnotify",
541
"version": "0.1.1"
542
+ },
543
+ {
544
+ "author": "stebalien",
545
+ "hash": "QmRAPFFaF7nrezCZQaLihyp2qbAXqSJU5WpvSpwroMv1Xt",
546
+ "name": "go-ipns",
547
+ "version": "0.1.1"
548
}
549
],
550
"gxVersion": "0.10.0",