From d8ffc6a4a955674abbe48f8c0c02dfa3c8786868 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Mon, 13 Mar 2023 19:13:57 +0100 Subject: [PATCH 1/3] added GenRandomKey function + tests --- table_refresh.go | 49 +++++++++++++++++++++++++- table_refresh_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++ util.go | 3 ++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/table_refresh.go b/table_refresh.go index 5156040..c370495 100644 --- a/table_refresh.go +++ b/table_refresh.go @@ -65,11 +65,58 @@ func (rt *RoutingTable) GenRandPeerID(targetCpl uint) (peer.ID, error) { // Convert to a known peer ID. key := keyPrefixMap[targetPrefix] - id := [34]byte{mh.SHA2_256, 32} + id := [Keysize + 2]byte{mh.SHA2_256, Keysize} binary.BigEndian.PutUint32(id[2:], key) return peer.ID(id[:]), nil } +// GenRandomKey generates a random key matching a provided Common Prefix Length (Cpl) wrt. the local identity +func (rt *RoutingTable) GenRandomKey(targetCpl uint) (ID, error) { + // targetCpl cannot be larger than the key size in bits + if targetCpl >= Keysize*8 { + return nil, fmt.Errorf("cannot generate peer ID for Cpl greater than key length") + } + + // the generated key must match the targetCpl first bits of the local key, + // the following bit is the inverse of the local key's bit at position targetCpl+1 + // and the remaining bits are randomly generated + // + // The returned ID takes the first targetCpl/8 bytes from the local key, the next byte is + // targetCpl%8 bits from the local key, 1 bit from the local key inverted and the remaining + // bits are random, and the last bytes are random + + // generate random bytes + nRandBytes := Keysize - (targetCpl+1)/8 + randBytes := make([]byte, nRandBytes) + _, err := rand.Read(randBytes) + if err != nil { + return nil, err + } + + // byte and bit offset for the given cpl + byteOffset := targetCpl / 8 + bitOffset := targetCpl % 8 + + // copy the local key to a new slice + randKey := make([]byte, len(rt.local)) + copy(randKey, []byte(rt.local)) + + // compute the mask for the local key, the first targetCpl bits must be the same as the local key + // hence the mask is 1s for bits up to targetCpl and 0s for the rest + localMask := (^byte(0)) << (8 - bitOffset) + // compute the bit that is flipped in the local key (at position targetCpl+1) + bucketBit := (byte(0x80) >> bitOffset) & ^rt.local[targetCpl/8] + + // first bitOffset bits are the same as the local key, the next bit is flipped and the remaining bits are random + randKey[byteOffset] = (localMask & randKey[byteOffset]) | bucketBit | (randBytes[0] & ((^localMask) >> 1)) + // the remaining bytes are random + for i := uint(1); i < nRandBytes; i++ { + randKey[byteOffset+i] = randBytes[i] + } + + return randKey, nil +} + // ResetCplRefreshedAtForID resets the refresh time for the Cpl of the given ID. func (rt *RoutingTable) ResetCplRefreshedAtForID(id ID, newTime time.Time) { cpl := CommonPrefixLen(id, rt.local) diff --git a/table_refresh_test.go b/table_refresh_test.go index e99a89a..97cda57 100644 --- a/table_refresh_test.go +++ b/table_refresh_test.go @@ -34,6 +34,86 @@ func TestGenRandPeerID(t *testing.T) { } } +func TestGenRandomKey(t *testing.T) { + // test can be run in parallel + t.Parallel() + + // run multiple occurences to make sure the test wasn't just lucky + for i := 0; i < 100; i++ { + // generate routing table with random local peer ID + local := test.RandPeerIDFatal(t) + m := pstore.NewMetrics() + rt, err := NewRoutingTable(1, ConvertPeerID(local), time.Hour, m, NoOpThreshold, nil) + require.NoError(t, err) + + // GenRandomKey fails for cpl >= 256 + _, err = rt.GenRandomKey(256) + require.Error(t, err) + _, err = rt.GenRandomKey(300) + require.Error(t, err) + + // bitwise comparison legend: + // O for same bit, X for different bit, ? for don't care + + // we compare the returned generated key with the local key + // for CPL = X, the first X bits should be the same, bit X+1 should be + // different, and the rest should be random / don't care + + // cpl = 0 should return a different first bit + // X??????? ???... + key0, err := rt.GenRandomKey(0) + require.NoError(t, err) + // most significant bit should be different + require.NotEqual(t, key0[0]>>7, rt.local[0]>>7) + + // cpl = 1 should return a different second bit + // OX?????? ???... + key1, err := rt.GenRandomKey(1) + require.NoError(t, err) + // MSB should be equal, as cpl = 1 + require.Equal(t, key1[0]>>7, rt.local[0]>>7) + // 2nd MSB should be different + require.NotEqual(t, (key1[0]<<1)>>6, (rt.local[0]<<1)>>6) + + // cpl = 2 should return a different third bit + // OOX????? ???... + key2, err := rt.GenRandomKey(2) + require.NoError(t, err) + // 2 MSB should be equal, as cpl = 2 + require.Equal(t, key2[0]>>6, rt.local[0]>>6) + // 3rd MSB should be different + require.NotEqual(t, (key2[0]<<2)>>5, (rt.local[0]<<2)>>5) + + // cpl = 7 should return a different eighth bit + // OOOOOOOX ???... + key7, err := rt.GenRandomKey(7) + require.NoError(t, err) + // 7 MSB should be equal, as cpl = 7 + require.Equal(t, key7[0]>>1, rt.local[0]>>1) + // 8th MSB should be different + require.NotEqual(t, key7[0]<<7, rt.local[0]<<7) + + // cpl = 8 should return a different ninth bit + // OOOOOOOO X???... + key8, err := rt.GenRandomKey(8) + require.NoError(t, err) + // 8 MSB should be equal, as cpl = 8 + require.Equal(t, key8[0], rt.local[0]) + // 9th MSB should be different + require.NotEqual(t, key8[1]>>7, rt.local[1]>>7) + + // cpl = 53 should return a different 54th bit + // OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOX?? ???... + key53, err := rt.GenRandomKey(53) + require.NoError(t, err) + // 53 MSB should be equal, as cpl = 53 + require.Equal(t, key53[:6], rt.local[:6]) + require.Equal(t, key53[6]>>3, rt.local[6]>>3) + // 54th MSB should be different + require.NotEqual(t, (key53[6]<<5)>>7, (rt.local[6]<<5)>>7) + } +} + func TestRefreshAndGetTrackedCpls(t *testing.T) { t.Parallel() diff --git a/util.go b/util.go index 7781110..04f128b 100644 --- a/util.go +++ b/util.go @@ -15,6 +15,9 @@ import ( // behaviour var ErrLookupFailure = errors.New("failed to find any peer in table") +// Keysize is the size of the Kademlia ID in bytes +const Keysize = 32 + // ID for IpfsDHT is in the XORKeySpace // // The type dht.ID signifies that its contents have been hashed from either a From 2d05bb074aa2a06a74158eb1f4150fa5fd30f859 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 14 Mar 2023 09:30:19 +0100 Subject: [PATCH 2/3] simplified code --- table_refresh.go | 61 +++++++++++++++++++----------------------------- util.go | 3 --- 2 files changed, 24 insertions(+), 40 deletions(-) diff --git a/table_refresh.go b/table_refresh.go index c370495..8e984f6 100644 --- a/table_refresh.go +++ b/table_refresh.go @@ -65,56 +65,43 @@ func (rt *RoutingTable) GenRandPeerID(targetCpl uint) (peer.ID, error) { // Convert to a known peer ID. key := keyPrefixMap[targetPrefix] - id := [Keysize + 2]byte{mh.SHA2_256, Keysize} + id := [32 + 2]byte{mh.SHA2_256, 32} binary.BigEndian.PutUint32(id[2:], key) return peer.ID(id[:]), nil } -// GenRandomKey generates a random key matching a provided Common Prefix Length (Cpl) wrt. the local identity +// GenRandomKey generates a random key matching a provided Common Prefix Length (Cpl) +// wrt. the local identity. The returned key matches the targetCpl first bits of the +// local key, the following bit is the inverse of the local key's bit at position +// targetCpl+1 and the remaining bits are randomly generated. func (rt *RoutingTable) GenRandomKey(targetCpl uint) (ID, error) { - // targetCpl cannot be larger than the key size in bits - if targetCpl >= Keysize*8 { + if int(targetCpl+1) >= len(rt.local)*8 { return nil, fmt.Errorf("cannot generate peer ID for Cpl greater than key length") } + partialOffset := targetCpl / 8 - // the generated key must match the targetCpl first bits of the local key, - // the following bit is the inverse of the local key's bit at position targetCpl+1 - // and the remaining bits are randomly generated - // - // The returned ID takes the first targetCpl/8 bytes from the local key, the next byte is - // targetCpl%8 bits from the local key, 1 bit from the local key inverted and the remaining - // bits are random, and the last bytes are random - - // generate random bytes - nRandBytes := Keysize - (targetCpl+1)/8 - randBytes := make([]byte, nRandBytes) - _, err := rand.Read(randBytes) + // output contains the first partialOffset bytes of the local key + // and the remaining bytes are random + output := make([]byte, len(rt.local)) + copy(output, rt.local[:partialOffset]) + _, err := rand.Read(output[partialOffset:]) if err != nil { return nil, err } - // byte and bit offset for the given cpl - byteOffset := targetCpl / 8 - bitOffset := targetCpl % 8 - - // copy the local key to a new slice - randKey := make([]byte, len(rt.local)) - copy(randKey, []byte(rt.local)) - - // compute the mask for the local key, the first targetCpl bits must be the same as the local key - // hence the mask is 1s for bits up to targetCpl and 0s for the rest - localMask := (^byte(0)) << (8 - bitOffset) - // compute the bit that is flipped in the local key (at position targetCpl+1) - bucketBit := (byte(0x80) >> bitOffset) & ^rt.local[targetCpl/8] - - // first bitOffset bits are the same as the local key, the next bit is flipped and the remaining bits are random - randKey[byteOffset] = (localMask & randKey[byteOffset]) | bucketBit | (randBytes[0] & ((^localMask) >> 1)) - // the remaining bytes are random - for i := uint(1); i < nRandBytes; i++ { - randKey[byteOffset+i] = randBytes[i] - } + remainingBits := targetCpl % 8 + orig := rt.local[partialOffset] + + origMask := ^uint8(0) << (8 - remainingBits) + randMask := ^origMask >> 1 + flippedBitOffset := remainingBits + 1 + flippedBitMask := uint8(1) << (8 - flippedBitOffset) + + // restore the remainingBits Most Significant Bits of orig + // and flip the flippedBitOffset-th bit of orig + output[partialOffset] = orig&origMask | (orig & flippedBitMask) ^ flippedBitMask | output[partialOffset]&randMask - return randKey, nil + return ID(output), nil } // ResetCplRefreshedAtForID resets the refresh time for the Cpl of the given ID. diff --git a/util.go b/util.go index 04f128b..7781110 100644 --- a/util.go +++ b/util.go @@ -15,9 +15,6 @@ import ( // behaviour var ErrLookupFailure = errors.New("failed to find any peer in table") -// Keysize is the size of the Kademlia ID in bytes -const Keysize = 32 - // ID for IpfsDHT is in the XORKeySpace // // The type dht.ID signifies that its contents have been hashed from either a From eedec94c42cdb2eb1fd75d9463c33c25aeeee894 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 14 Mar 2023 16:26:36 +0100 Subject: [PATCH 3/3] updated remainingBits count --- table_refresh.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/table_refresh.go b/table_refresh.go index 8e984f6..446d4e7 100644 --- a/table_refresh.go +++ b/table_refresh.go @@ -89,13 +89,13 @@ func (rt *RoutingTable) GenRandomKey(targetCpl uint) (ID, error) { return nil, err } - remainingBits := targetCpl % 8 + remainingBits := 8 - targetCpl%8 orig := rt.local[partialOffset] - origMask := ^uint8(0) << (8 - remainingBits) + origMask := ^uint8(0) << remainingBits randMask := ^origMask >> 1 - flippedBitOffset := remainingBits + 1 - flippedBitMask := uint8(1) << (8 - flippedBitOffset) + flippedBitOffset := remainingBits - 1 + flippedBitMask := uint8(1) << flippedBitOffset // restore the remainingBits Most Significant Bits of orig // and flip the flippedBitOffset-th bit of orig