fix: move visual-identity service to protocol/identity

It is required to be called before RPC server is running on client side
This commit is contained in:
Patryk Osmaczko 2022-03-17 17:58:35 +01:00 committed by osmaczko
parent 7ef8bc68c8
commit d0f4a94f75
13 changed files with 403 additions and 445 deletions

View File

@ -26,6 +26,8 @@ import (
"github.com/status-im/status-go/profiling" "github.com/status-im/status-go/profiling"
protocol "github.com/status-im/status-go/protocol" protocol "github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/identity/alias" "github.com/status-im/status-go/protocol/identity/alias"
"github.com/status-im/status-go/protocol/identity/colorhash"
"github.com/status-im/status-go/protocol/identity/emojihash"
"github.com/status-im/status-go/server" "github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/typeddata" "github.com/status-im/status-go/services/typeddata"
@ -652,6 +654,14 @@ func Identicon(pk string) string {
return identicon return identicon
} }
func EmojiHash(pk string) string {
return prepareJSONResponse(emojihash.GenerateFor(pk))
}
func ColorHash(pk string) string {
return prepareJSONResponse(colorhash.GenerateFor(pk))
}
func ValidateMnemonic(mnemonic string) string { func ValidateMnemonic(mnemonic string) string {
m := extkeys.NewMnemonic() m := extkeys.NewMnemonic()
err := m.ValidateMnemonic(mnemonic, extkeys.Language(0)) err := m.ValidateMnemonic(mnemonic, extkeys.Language(0))

View File

@ -45,7 +45,6 @@ import (
"github.com/status-im/status-go/services/status" "github.com/status-im/status-go/services/status"
"github.com/status-im/status-go/services/stickers" "github.com/status-im/status-go/services/stickers"
"github.com/status-im/status-go/services/subscriptions" "github.com/status-im/status-go/services/subscriptions"
visualIdentity "github.com/status-im/status-go/services/visual-identity"
"github.com/status-im/status-go/services/wakuext" "github.com/status-im/status-go/services/wakuext"
"github.com/status-im/status-go/services/wakuv2ext" "github.com/status-im/status-go/services/wakuv2ext"
"github.com/status-im/status-go/services/wallet" "github.com/status-im/status-go/services/wallet"
@ -118,7 +117,6 @@ type StatusNode struct {
gifSrvc *gif.Service gifSrvc *gif.Service
stickersSrvc *stickers.Service stickersSrvc *stickers.Service
chatSrvc *chat.Service chatSrvc *chat.Service
visualIdentitySrvc *visualIdentity.Service
} }
// New makes new instance of StatusNode. // New makes new instance of StatusNode.
@ -429,7 +427,6 @@ func (n *StatusNode) stop() error {
n.wakuV2ExtSrvc = nil n.wakuV2ExtSrvc = nil
n.ensSrvc = nil n.ensSrvc = nil
n.stickersSrvc = nil n.stickersSrvc = nil
n.visualIdentitySrvc = nil
n.publicMethods = make(map[string]bool) n.publicMethods = make(map[string]bool)
return nil return nil

View File

@ -38,7 +38,6 @@ import (
"github.com/status-im/status-go/services/status" "github.com/status-im/status-go/services/status"
"github.com/status-im/status-go/services/stickers" "github.com/status-im/status-go/services/stickers"
"github.com/status-im/status-go/services/subscriptions" "github.com/status-im/status-go/services/subscriptions"
visualIdentity "github.com/status-im/status-go/services/visual-identity"
"github.com/status-im/status-go/services/wakuext" "github.com/status-im/status-go/services/wakuext"
"github.com/status-im/status-go/services/wakuv2ext" "github.com/status-im/status-go/services/wakuv2ext"
"github.com/status-im/status-go/services/wallet" "github.com/status-im/status-go/services/wallet"
@ -79,12 +78,6 @@ func (b *StatusNode) initServices(config *params.NodeConfig) error {
services = append(services, b.gifService()) services = append(services, b.gifService())
services = append(services, b.ChatService()) services = append(services, b.ChatService())
visualIdentitySrvc, err := b.visualIdentityService()
if err != nil {
return err
}
services = append(services, visualIdentitySrvc)
if config.WakuConfig.Enabled { if config.WakuConfig.Enabled {
wakuService, err := b.wakuService(&config.WakuConfig, &config.ClusterConfig) wakuService, err := b.wakuService(&config.WakuConfig, &config.ClusterConfig)
if err != nil { if err != nil {
@ -395,21 +388,6 @@ func (b *StatusNode) gifService() *gif.Service {
return b.gifSrvc return b.gifSrvc
} }
func (b *StatusNode) visualIdentityService() (*visualIdentity.Service, error) {
if b.visualIdentitySrvc != nil {
return b.visualIdentitySrvc, nil
}
srvc := visualIdentity.NewService()
err := srvc.Init()
if err != nil {
return nil, err
}
b.visualIdentitySrvc = srvc
return b.visualIdentitySrvc, nil
}
func (b *StatusNode) ChatService() *chat.Service { func (b *StatusNode) ChatService() *chat.Service {
if b.chatSrvc == nil { if b.chatSrvc == nil {
b.chatSrvc = chat.NewService(b.appDB) b.chatSrvc = chat.NewService(b.appDB)

View File

@ -0,0 +1,73 @@
package colorhash
import (
"math/big"
"github.com/status-im/status-go/protocol/identity"
)
const (
colorHashSegmentMaxLen = 5
colorHashColorsCount = 32
)
var colorHashAlphabet [][]int
func GenerateFor(pubkey string) (hash [][]int, err error) {
if len(colorHashAlphabet) == 0 {
colorHashAlphabet = makeColorHashAlphabet(colorHashSegmentMaxLen, colorHashColorsCount)
}
compressedKey, err := identity.ToCompressedKey(pubkey)
if err != nil {
return nil, err
}
slices, err := identity.Slices(compressedKey)
if err != nil {
return nil, err
}
return toColorHash(new(big.Int).SetBytes(slices[2]), &colorHashAlphabet, colorHashColorsCount), nil
}
// [[1 0] [1 1] [1 2] ... [units, colors-1]]
// [3 12] => 3 units length, 12 color index
func makeColorHashAlphabet(units, colors int) (res [][]int) {
res = make([][]int, units*colors)
idx := 0
for i := 0; i < units; i++ {
for j := 0; j < colors; j++ {
res[idx] = make([]int, 2)
res[idx][0] = i + 1
res[idx][1] = j
idx++
}
}
return
}
func toColorHash(value *big.Int, alphabet *[][]int, colorsCount int) (hash [][]int) {
alphabetLen := len(*alphabet)
indexes := identity.ToBigBase(value, uint64(alphabetLen))
hash = make([][](int), len(indexes))
for i, v := range indexes {
hash[i] = make([](int), 2)
hash[i][0] = (*alphabet)[v][0]
hash[i][1] = (*alphabet)[v][1]
}
// colors can't repeat themselves
// this makes color hash not fully collision resistant
prevColorIdx := hash[0][1]
hashLen := len(hash)
for i := 1; i < hashLen; i++ {
colorIdx := hash[i][1]
if colorIdx == prevColorIdx {
hash[i][1] = (colorIdx + 1) % colorsCount
}
prevColorIdx = hash[i][1]
}
return
}

View File

@ -0,0 +1,57 @@
package colorhash
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
"github.com/status-im/status-go/protocol/identity"
)
func TestGenerateFor(t *testing.T) {
checker := func(pubkey string, expected *[][](int)) {
colorhash, err := GenerateFor(pubkey)
require.NoError(t, err)
if !reflect.DeepEqual(colorhash, *expected) {
t.Fatalf("invalid emojihash %v != %v", colorhash, *expected)
}
}
checker("0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8",
&[][]int{{3, 30}, {2, 10}, {5, 5}, {3, 14}, {5, 4}, {4, 19}, {3, 16}, {4, 0}, {5, 28}, {4, 13}, {4, 15}})
}
func TestColorHashOfInvalidKey(t *testing.T) {
checker := func(pubkey string) {
_, err := GenerateFor(pubkey)
require.Error(t, err)
}
checker("abc")
checker("0x01")
checker("0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8")
checker("0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8")
}
func TestColorHash(t *testing.T) {
alphabet := makeColorHashAlphabet(4, 4)
checker := func(valueStr string, expected *[][](int)) {
value := identity.ToBigInt(t, valueStr)
res := toColorHash(value, &alphabet, 4)
if !reflect.DeepEqual(res, *expected) {
t.Fatalf("invalid colorhash conversion %v != %v", res, *expected)
}
}
checker("0x0", &[][]int{{1, 0}})
checker("0x1", &[][]int{{1, 1}})
checker("0x4", &[][]int{{2, 0}})
checker("0xF", &[][]int{{4, 3}})
// oops, collision
checker("0xFF", &[][]int{{4, 3}, {4, 0}})
checker("0xFC", &[][]int{{4, 3}, {4, 0}})
checker("0xFFFF", &[][]int{{4, 3}, {4, 0}, {4, 3}, {4, 0}})
}

View File

@ -0,0 +1,94 @@
package emojihash
import (
"bufio"
"bytes"
"errors"
"math/big"
"strings"
"github.com/status-im/status-go/protocol/identity"
"github.com/status-im/status-go/static"
)
const (
emojiAlphabetLen = 2757 // 20bytes of data described by 14 emojis requires at least 2757 length alphabet
emojiHashLen = 14
)
var emojisAlphabet []string
func GenerateFor(pubkey string) ([]string, error) {
if len(emojisAlphabet) == 0 {
alphabet, err := loadAlphabet()
if err != nil {
return nil, err
}
emojisAlphabet = *alphabet
}
compressedKey, err := identity.ToCompressedKey(pubkey)
if err != nil {
return nil, err
}
slices, err := identity.Slices(compressedKey)
if err != nil {
return nil, err
}
return toEmojiHash(new(big.Int).SetBytes(slices[1]), emojiHashLen, &emojisAlphabet)
}
func loadAlphabet() (*[]string, error) {
data, err := static.Asset("emojis.txt")
if err != nil {
return nil, err
}
alphabet := make([]string, 0, emojiAlphabetLen)
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
alphabet = append(alphabet, strings.Replace(scanner.Text(), "\n", "", -1))
}
// current alphabet contains more emojis than needed, just in case some emojis needs to be removed
// make sure only necessary part is loaded
if len(alphabet) > emojiAlphabetLen {
alphabet = alphabet[:emojiAlphabetLen]
}
return &alphabet, nil
}
func toEmojiHash(value *big.Int, hashLen int, alphabet *[]string) (hash []string, err error) {
valueBitLen := value.BitLen()
alphabetLen := new(big.Int).SetInt64(int64(len(*alphabet)))
indexes := identity.ToBigBase(value, alphabetLen.Uint64())
if hashLen == 0 {
hashLen = len(indexes)
} else if hashLen > len(indexes) {
prependLen := hashLen - len(indexes)
for i := 0; i < prependLen; i++ {
indexes = append([](uint64){0}, indexes...)
}
}
// alphabetLen^hashLen
possibleCombinations := new(big.Int).Exp(alphabetLen, new(big.Int).SetInt64(int64(hashLen)), nil)
// 2^valueBitLen
requiredCombinations := new(big.Int).Exp(new(big.Int).SetInt64(2), new(big.Int).SetInt64(int64(valueBitLen)), nil)
if possibleCombinations.Cmp(requiredCombinations) == -1 {
return nil, errors.New("alphabet or hash length is too short to encode given value")
}
for _, v := range indexes {
hash = append(hash, (*alphabet)[v])
}
return hash, nil
}

View File

@ -1,27 +1,17 @@
package visualidentity package emojihash
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/status-im/status-go/protocol/identity"
) )
func setupTestAPI(t *testing.T) *API { func TestGenerateFor(t *testing.T) {
api := NewAPI()
alphabet, err := LoadAlphabet()
require.NoError(t, err)
api.emojisAlphabet = alphabet
return api
}
func TestEmojiHashOf(t *testing.T) {
api := setupTestAPI(t)
checker := func(pubkey string, expected *[](string)) { checker := func(pubkey string, expected *[](string)) {
emojihash, err := api.EmojiHashOf(pubkey) emojihash, err := GenerateFor(pubkey)
require.NoError(t, err) require.NoError(t, err)
if !reflect.DeepEqual(emojihash, *expected) { if !reflect.DeepEqual(emojihash, *expected) {
t.Fatalf("invalid emojihash %v != %v", emojihash, *expected) t.Fatalf("invalid emojihash %v != %v", emojihash, *expected)
@ -35,34 +25,15 @@ func TestEmojiHashOf(t *testing.T) {
&[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀"}) &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀"})
checker("0x04000000000000000000000000000000000000000010000000000000000000000033600332D373318ECC2F212A30A5750D2EAC827B6A32B33D326CCF369B12B1BE", checker("0x04000000000000000000000000000000000000000010000000000000000000000033600332D373318ECC2F212A30A5750D2EAC827B6A32B33D326CCF369B12B1BE",
&[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (*api.emojisAlphabet)[1]}) &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (emojisAlphabet)[1]})
checker("0x040000000000000000000000000000000000000000200000000000000000000000353050BFE33B724E60A0C600FBA565A9B62217B1BD35BF9848F2AB847C598B30", checker("0x040000000000000000000000000000000000000000200000000000000000000000353050BFE33B724E60A0C600FBA565A9B62217B1BD35BF9848F2AB847C598B30",
&[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (*api.emojisAlphabet)[2]}) &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (emojisAlphabet)[2]})
} }
func TestColorHashOf(t *testing.T) { func TestEmojiHashOfInvalidKey(t *testing.T) {
api := NewAPI()
checker := func(pubkey string, expected *[][](int)) {
colorhash, err := api.ColorHashOf(pubkey)
require.NoError(t, err)
if !reflect.DeepEqual(colorhash, *expected) {
t.Fatalf("invalid emojihash %v != %v", colorhash, *expected)
}
}
checker("0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8",
&[][]int{{3, 30}, {2, 10}, {5, 5}, {3, 14}, {5, 4}, {4, 19}, {3, 16}, {4, 0}, {5, 28}, {4, 13}, {4, 15}})
}
func TestHashesOfInvalidKey(t *testing.T) {
api := setupTestAPI(t)
checker := func(pubkey string) { checker := func(pubkey string) {
_, err := api.EmojiHashOf(pubkey) _, err := GenerateFor(pubkey)
require.Error(t, err)
_, err = api.ColorHashOf(pubkey)
require.Error(t, err) require.Error(t, err)
} }
checker("abc") checker("abc")
@ -70,3 +41,30 @@ func TestHashesOfInvalidKey(t *testing.T) {
checker("0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8") checker("0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8")
checker("0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8") checker("0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8")
} }
func TestToEmojiHash(t *testing.T) {
alphabet := [](string){"😇", "🤐", "🥵", "🙊", "🤌"}
checker := func(valueStr string, hashLen int, expected *[](string)) {
value := identity.ToBigInt(t, valueStr)
res, err := toEmojiHash(value, hashLen, &alphabet)
require.NoError(t, err)
if !reflect.DeepEqual(res, *expected) {
t.Fatalf("invalid emojihash conversion %v != %v", res, *expected)
}
}
checker("777", 5, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"})
checker("777", 0, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"})
checker("777", 10, &[](string){"😇", "😇", "😇", "😇", "😇", "🤐", "🤐", "🤐", "😇", "🥵"})
// 20bytes of data described by 14 emojis requires at least 2757 length alphabet
alphabet = make([](string), 2757)
val := identity.ToBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") // 20 bytes
_, err := toEmojiHash(val, 14, &alphabet)
require.NoError(t, err)
alphabet = make([](string), 2757-1)
_, err = toEmojiHash(val, 14, &alphabet)
require.Error(t, err)
}

View File

@ -0,0 +1,68 @@
package identity
import (
"errors"
"fmt"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
)
func ToBigBase(value *big.Int, base uint64) (res [](uint64)) {
toBigBaseImpl(value, base, &res)
return
}
func toBigBaseImpl(value *big.Int, base uint64, res *[](uint64)) {
bigBase := new(big.Int).SetUint64(base)
quotient := new(big.Int).Div(value, bigBase)
if quotient.Cmp(new(big.Int).SetUint64(0)) != 0 {
toBigBaseImpl(quotient, base, res)
}
*res = append(*res, new(big.Int).Mod(value, bigBase).Uint64())
}
// compressedPubKey = |1.5 bytes chars cutoff|20 bytes emoji hash|10 bytes color hash|1.5 bytes chars cutoff|
func Slices(compressedPubkey []byte) (res [4][]byte, err error) {
if len(compressedPubkey) != 33 {
return res, errors.New("incorrect compressed pubkey")
}
getSlice := func(low, high int, and string, rsh uint) []byte {
sliceValue := new(big.Int).SetBytes(compressedPubkey[low:high])
andValue, _ := new(big.Int).SetString(and, 0)
andRes := new(big.Int).And(sliceValue, andValue)
return new(big.Int).Rsh(andRes, rsh).Bytes()
}
res[0] = getSlice(0, 2, "0xFFF0", 4)
res[1] = getSlice(1, 22, "0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0", 4)
res[2] = getSlice(21, 32, "0x0FFFFFFFFFFFFFFFFFFFF0", 4)
res[3] = getSlice(31, 33, "0x0FFF", 0)
return res, nil
}
func ToCompressedKey(pubkey string) ([]byte, error) {
pubkeyValue, ok := new(big.Int).SetString(pubkey, 0)
if !ok {
return nil, fmt.Errorf("invalid pubkey: %s", pubkey)
}
x, y := secp256k1.S256().Unmarshal(pubkeyValue.Bytes())
if x == nil || !secp256k1.S256().IsOnCurve(x, y) {
return nil, fmt.Errorf("invalid pubkey: %s", pubkey)
}
return secp256k1.CompressPubkey(x, y), nil
}
func ToBigInt(t *testing.T, str string) *big.Int {
res, ok := new(big.Int).SetString(str, 0)
if !ok {
t.Errorf("invalid conversion to int from %s", str)
}
return res
}

View File

@ -0,0 +1,65 @@
package identity
import (
"math"
"math/big"
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func TestToBigBase(t *testing.T) {
checker := func(value *big.Int, base uint64, expected *[](uint64)) {
res := ToBigBase(value, base)
if !reflect.DeepEqual(res, *expected) {
t.Fatalf("invalid big base conversion %v != %v", res, *expected)
}
}
lengthChecker := func(value *big.Int, base, expectedLength uint64) {
res := ToBigBase(value, base)
if len(res) != int(expectedLength) {
t.Fatalf("invalid big base conversion %d != %d", len(res), expectedLength)
}
}
checker(new(big.Int).SetUint64(15), 16, &[](uint64){15})
checker(new(big.Int).SetUint64(495), 16, &[](uint64){1, 14, 15})
checker(new(big.Int).SetUint64(495), 30, &[](uint64){16, 15})
checker(new(big.Int).SetUint64(495), 1024, &[](uint64){495})
checker(new(big.Int).SetUint64(2048), 1024, &[](uint64){2, 0})
base := uint64(math.Pow(2, 7*4))
checker(ToBigInt(t, "0xFFFFFFFFFFFFFF"), base, &[](uint64){base - 1, base - 1})
val := ToBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
lengthChecker(val, 2757, 14)
lengthChecker(val, 2756, 15)
}
func TestSlices(t *testing.T) {
checker := func(compressedKey, charsCutoffA, emojiHash, colorHash, charsCutoffB string) {
slices, err := Slices(ToBigInt(t, compressedKey).Bytes())
require.NoError(t, err)
sliceChecker := func(idx int, value *big.Int) {
if !reflect.DeepEqual(slices[idx], value.Bytes()) {
t.Fatalf("invalid slice (%d) %v != %v", idx, slices[idx], value.Bytes())
}
}
sliceChecker(0, ToBigInt(t, charsCutoffA))
sliceChecker(1, ToBigInt(t, emojiHash))
sliceChecker(2, ToBigInt(t, colorHash))
sliceChecker(3, ToBigInt(t, charsCutoffB))
}
checker("0x03086138b210f21d41c757ae8a5d2a4cb29c1350f7389517608378ebd9efcf4a55", "0x030", "0x86138b210f21d41c757ae8a5d2a4cb29c1350f73", "0x89517608378ebd9efcf4", "0xa55")
checker("0x020000000000000000000000000000000000000000100000000000000000000000", "0x020", "0x0000000000000000000000000000000000000001", "0x00000000000000000000", "0x000")
}
func TestSlicesInvalid(t *testing.T) {
_, err := Slices(ToBigInt(t, "0x01").Bytes())
require.Error(t, err)
}

View File

@ -1,92 +0,0 @@
package visualidentity
import (
"bufio"
"bytes"
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/static"
)
const (
emojiAlphabetLen = 2757 // 20bytes of data described by 14 emojis requires at least 2757 length alphabet
emojiHashLen = 14
colorHashSegmentMaxLen = 5
colorHashColorsCount = 32
)
func NewAPI() *API {
colorHashAlphabet := MakeColorHashAlphabet(colorHashSegmentMaxLen, colorHashColorsCount)
return &API{
emojisAlphabet: &[]string{},
colorHashAlphabet: &colorHashAlphabet,
}
}
type API struct {
emojisAlphabet *[]string
colorHashAlphabet *[][]int
}
func (api *API) EmojiHashOf(pubkey string) (hash []string, err error) {
log.Info("[VisualIdentityAPI::EmojiHashOf]")
slices, err := slices(pubkey)
if err != nil {
return nil, err
}
return ToEmojiHash(new(big.Int).SetBytes(slices[1]), emojiHashLen, api.emojisAlphabet)
}
func (api *API) ColorHashOf(pubkey string) (hash [][]int, err error) {
log.Info("[VisualIdentityAPI::ColorHashOf]")
slices, err := slices(pubkey)
if err != nil {
return nil, err
}
return ToColorHash(new(big.Int).SetBytes(slices[2]), api.colorHashAlphabet, colorHashColorsCount), nil
}
func LoadAlphabet() (*[]string, error) {
data, err := static.Asset("emojis.txt")
if err != nil {
return nil, err
}
alphabet := make([]string, 0, emojiAlphabetLen)
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
alphabet = append(alphabet, strings.Replace(scanner.Text(), "\n", "", -1))
}
// current alphabet contains more emojis than needed, just in case some emojis needs to be removed
// make sure only necessary part is loaded
if len(alphabet) > emojiAlphabetLen {
alphabet = alphabet[:emojiAlphabetLen]
}
return &alphabet, nil
}
func slices(pubkey string) (res [4][]byte, err error) {
pubkeyValue, ok := new(big.Int).SetString(pubkey, 0)
if !ok {
return res, fmt.Errorf("invalid pubkey: %s", pubkey)
}
x, y := secp256k1.S256().Unmarshal(pubkeyValue.Bytes())
if x == nil || !secp256k1.S256().IsOnCurve(x, y) {
return res, fmt.Errorf("invalid pubkey: %s", pubkey)
}
compressedKey := secp256k1.CompressPubkey(x, y)
return Slices(compressedKey)
}

View File

@ -1,53 +0,0 @@
package visualidentity
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// Service represents out own implementation of Identity Visual Representation.
type Service struct {
api *API
}
// New returns a new Service.
func NewService() *Service {
return &Service{
api: NewAPI(),
}
}
func (s *Service) Init() error {
alphabet, err := LoadAlphabet()
if err == nil {
s.api.emojisAlphabet = alphabet
}
return err
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "visualIdentity",
Version: "0.1.0",
Service: s.api,
Public: true,
},
}
}
// Start is run when a service is started.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
return nil
}

View File

@ -1,114 +0,0 @@
package visualidentity
import (
"errors"
"math/big"
)
func ToBigBase(value *big.Int, base uint64) (res [](uint64)) {
toBigBaseImpl(value, base, &res)
return
}
func toBigBaseImpl(value *big.Int, base uint64, res *[](uint64)) {
bigBase := new(big.Int).SetUint64(base)
quotient := new(big.Int).Div(value, bigBase)
if quotient.Cmp(new(big.Int).SetUint64(0)) != 0 {
toBigBaseImpl(quotient, base, res)
}
*res = append(*res, new(big.Int).Mod(value, bigBase).Uint64())
}
func ToEmojiHash(value *big.Int, hashLen int, alphabet *[]string) (hash []string, err error) {
valueBitLen := value.BitLen()
alphabetLen := new(big.Int).SetInt64(int64(len(*alphabet)))
indexes := ToBigBase(value, alphabetLen.Uint64())
if hashLen == 0 {
hashLen = len(indexes)
} else if hashLen > len(indexes) {
prependLen := hashLen - len(indexes)
for i := 0; i < prependLen; i++ {
indexes = append([](uint64){0}, indexes...)
}
}
// alphabetLen^hashLen
possibleCombinations := new(big.Int).Exp(alphabetLen, new(big.Int).SetInt64(int64(hashLen)), nil)
// 2^valueBitLen
requiredCombinations := new(big.Int).Exp(new(big.Int).SetInt64(2), new(big.Int).SetInt64(int64(valueBitLen)), nil)
if possibleCombinations.Cmp(requiredCombinations) == -1 {
return nil, errors.New("alphabet or hash length is too short to encode given value")
}
for _, v := range indexes {
hash = append(hash, (*alphabet)[v])
}
return hash, nil
}
// compressedPubKey = |1.5 bytes chars cutoff|20 bytes emoji hash|10 bytes color hash|1.5 bytes chars cutoff|
func Slices(compressedPubkey []byte) (res [4][]byte, err error) {
if len(compressedPubkey) != 33 {
return res, errors.New("incorrect compressed pubkey")
}
getSlice := func(low, high int, and string, rsh uint) []byte {
sliceValue := new(big.Int).SetBytes(compressedPubkey[low:high])
andValue, _ := new(big.Int).SetString(and, 0)
andRes := new(big.Int).And(sliceValue, andValue)
return new(big.Int).Rsh(andRes, rsh).Bytes()
}
res[0] = getSlice(0, 2, "0xFFF0", 4)
res[1] = getSlice(1, 22, "0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0", 4)
res[2] = getSlice(21, 32, "0x0FFFFFFFFFFFFFFFFFFFF0", 4)
res[3] = getSlice(31, 33, "0x0FFF", 0)
return res, nil
}
// [[1 0] [1 1] [1 2] ... [units, colors-1]]
// [3 12] => 3 units length, 12 color index
func MakeColorHashAlphabet(units, colors int) (res [][]int) {
res = make([][]int, units*colors)
idx := 0
for i := 0; i < units; i++ {
for j := 0; j < colors; j++ {
res[idx] = make([]int, 2)
res[idx][0] = i + 1
res[idx][1] = j
idx++
}
}
return
}
func ToColorHash(value *big.Int, alphabet *[][]int, colorsCount int) (hash [][]int) {
alphabetLen := len(*alphabet)
indexes := ToBigBase(value, uint64(alphabetLen))
hash = make([][](int), len(indexes))
for i, v := range indexes {
hash[i] = make([](int), 2)
hash[i][0] = (*alphabet)[v][0]
hash[i][1] = (*alphabet)[v][1]
}
// colors can't repeat themselves
// this makes color hash not fully collision resistant
prevColorIdx := hash[0][1]
hashLen := len(hash)
for i := 1; i < hashLen; i++ {
colorIdx := hash[i][1]
if colorIdx == prevColorIdx {
hash[i][1] = (colorIdx + 1) % colorsCount
}
prevColorIdx = hash[i][1]
}
return
}

View File

@ -1,123 +0,0 @@
package visualidentity
import (
"math"
"math/big"
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func TestToBigBase(t *testing.T) {
checker := func(value *big.Int, base uint64, expected *[](uint64)) {
res := ToBigBase(value, base)
if !reflect.DeepEqual(res, *expected) {
t.Fatalf("invalid big base conversion %v != %v", res, *expected)
}
}
lengthChecker := func(value *big.Int, base, expectedLength uint64) {
res := ToBigBase(value, base)
if len(res) != int(expectedLength) {
t.Fatalf("invalid big base conversion %d != %d", len(res), expectedLength)
}
}
checker(new(big.Int).SetUint64(15), 16, &[](uint64){15})
checker(new(big.Int).SetUint64(495), 16, &[](uint64){1, 14, 15})
checker(new(big.Int).SetUint64(495), 30, &[](uint64){16, 15})
checker(new(big.Int).SetUint64(495), 1024, &[](uint64){495})
checker(new(big.Int).SetUint64(2048), 1024, &[](uint64){2, 0})
base := uint64(math.Pow(2, 7*4))
checker(toBigInt(t, "0xFFFFFFFFFFFFFF"), base, &[](uint64){base - 1, base - 1})
val := toBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
lengthChecker(val, 2757, 14)
lengthChecker(val, 2756, 15)
}
func TestToEmojiHash(t *testing.T) {
alphabet := [](string){"😇", "🤐", "🥵", "🙊", "🤌"}
checker := func(valueStr string, hashLen int, expected *[](string)) {
value := toBigInt(t, valueStr)
res, err := ToEmojiHash(value, hashLen, &alphabet)
require.NoError(t, err)
if !reflect.DeepEqual(res, *expected) {
t.Fatalf("invalid emojihash conversion %v != %v", res, *expected)
}
}
checker("777", 5, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"})
checker("777", 0, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"})
checker("777", 10, &[](string){"😇", "😇", "😇", "😇", "😇", "🤐", "🤐", "🤐", "😇", "🥵"})
// 20bytes of data described by 14 emojis requires at least 2757 length alphabet
alphabet = make([](string), 2757)
val := toBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") // 20 bytes
_, err := ToEmojiHash(val, 14, &alphabet)
require.NoError(t, err)
alphabet = make([](string), 2757-1)
_, err = ToEmojiHash(val, 14, &alphabet)
require.Error(t, err)
}
func TestSlices(t *testing.T) {
checker := func(compressedKey, charsCutoffA, emojiHash, colorHash, charsCutoffB string) {
slices, err := Slices(toBigInt(t, compressedKey).Bytes())
require.NoError(t, err)
sliceChecker := func(idx int, value *big.Int) {
if !reflect.DeepEqual(slices[idx], value.Bytes()) {
t.Fatalf("invalid slice (%d) %v != %v", idx, slices[idx], value.Bytes())
}
}
sliceChecker(0, toBigInt(t, charsCutoffA))
sliceChecker(1, toBigInt(t, emojiHash))
sliceChecker(2, toBigInt(t, colorHash))
sliceChecker(3, toBigInt(t, charsCutoffB))
}
checker("0x03086138b210f21d41c757ae8a5d2a4cb29c1350f7389517608378ebd9efcf4a55", "0x030", "0x86138b210f21d41c757ae8a5d2a4cb29c1350f73", "0x89517608378ebd9efcf4", "0xa55")
checker("0x020000000000000000000000000000000000000000100000000000000000000000", "0x020", "0x0000000000000000000000000000000000000001", "0x00000000000000000000", "0x000")
}
func TestSlicesInvalid(t *testing.T) {
_, err := Slices(toBigInt(t, "0x01").Bytes())
require.Error(t, err)
}
func TestColorHash(t *testing.T) {
alphabet := MakeColorHashAlphabet(4, 4)
checker := func(valueStr string, expected *[][](int)) {
value := toBigInt(t, valueStr)
res := ToColorHash(value, &alphabet, 4)
if !reflect.DeepEqual(res, *expected) {
t.Fatalf("invalid colorhash conversion %v != %v", res, *expected)
}
}
checker("0x0", &[][]int{{1, 0}})
checker("0x1", &[][]int{{1, 1}})
checker("0x4", &[][]int{{2, 0}})
checker("0xF", &[][]int{{4, 3}})
// oops, collision
checker("0xFF", &[][]int{{4, 3}, {4, 0}})
checker("0xFC", &[][]int{{4, 3}, {4, 0}})
checker("0xFFFF", &[][]int{{4, 3}, {4, 0}, {4, 3}, {4, 0}})
}
func toBigInt(t *testing.T, str string) *big.Int {
res, ok := new(big.Int).SetString(str, 0)
if !ok {
t.Errorf("invalid conversion to int from %s", str)
}
return res
}