feat: add visual identity service (#2550)

This commit is contained in:
osmaczko 2022-03-11 22:17:48 +01:00 committed by GitHub
parent 442d24a79f
commit 9930dbe456
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 3323 additions and 20 deletions

1
.gitignore vendored
View file

@ -25,6 +25,7 @@
vendor/github.com/ethereum/go-ethereum/vendor
node_modules/
tags
build/
#*
.#*

View file

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

View file

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

View file

@ -0,0 +1,92 @@
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

@ -0,0 +1,72 @@
package visualidentity
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func setupTestAPI(t *testing.T) *API {
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)) {
emojihash, err := api.EmojiHashOf(pubkey)
require.NoError(t, err)
if !reflect.DeepEqual(emojihash, *expected) {
t.Fatalf("invalid emojihash %v != %v", emojihash, *expected)
}
}
checker("0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8",
&[](string){"👦🏽", "🦹🏻", "👶🏿", "🛁", "🌁", "🙌🏻", "🙇🏽‍♂️", "🙌🏾", "🤥", "🐛", "👩🏽‍🔧", "🔧", "⚙️", "🧒🏽"})
checker("0x0400000000000000000000000000000000000000000000000000000000000000014218F20AE6C646B363DB68605822FB14264CA8D2587FDD6FBC750D587E76A7EE",
&[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀"})
checker("0x04000000000000000000000000000000000000000010000000000000000000000033600332D373318ECC2F212A30A5750D2EAC827B6A32B33D326CCF369B12B1BE",
&[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (*api.emojisAlphabet)[1]})
checker("0x040000000000000000000000000000000000000000200000000000000000000000353050BFE33B724E60A0C600FBA565A9B62217B1BD35BF9848F2AB847C598B30",
&[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (*api.emojisAlphabet)[2]})
}
func TestColorHashOf(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) {
_, err := api.EmojiHashOf(pubkey)
require.Error(t, err)
_, err = api.ColorHashOf(pubkey)
require.Error(t, err)
}
checker("abc")
checker("0x01")
checker("0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8")
checker("0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8")
}

View file

@ -0,0 +1,53 @@
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

@ -0,0 +1,114 @@
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

@ -0,0 +1,123 @@
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
}

File diff suppressed because one or more lines are too long

2800
static/emojis.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
// Package static embeds static (JS, HTML) resources right into the binaries
package static
//go:generate go-bindata -pkg static -o bindata.go ../config/... ./keys
//go:generate go-bindata -pkg static -o bindata.go emojis.txt ../config/... ./keys