From 20399f121b4578feb602a30f40e8b66498d7b9c1 Mon Sep 17 00:00:00 2001 From: Kleissner Date: Fri, 27 Aug 2021 06:15:37 +0200 Subject: [PATCH] Rewrite of profile records on blockchain. This allows to add/update profile data (including future fields and blobs) at any time. --- Block Records.go | 204 ++++++++++++++++++++++++++++++++++++++--------- Test_test.go | 15 +++- 2 files changed, 180 insertions(+), 39 deletions(-) diff --git a/Block Records.go b/Block Records.go index 6efaeb4..1033184 100644 --- a/Block Records.go +++ b/Block Records.go @@ -34,7 +34,6 @@ import ( "math" "time" - "github.com/PeernetOfficial/core/sanitize" "github.com/google/uuid" ) @@ -42,7 +41,7 @@ import ( // RecordTypeX defines the type of the record const ( - RecordTypeUsername = 0 // Username. Arbitrary name defined by the user. + RecordTypeProfile = 0 // Profile data about the end user RecordTypeTagData = 1 // Tag data record to be referenced by one or multiple tags. Only valid in the context of the current block. RecordTypeFile = 2 // File RecordTypeDelete = 3 // Delete previous record by ID. @@ -51,12 +50,6 @@ const ( RecordTypeContentReport = 6 // Content report (negative). ) -// BlockRecordUser specifies user information -type BlockRecordUser struct { - Name string // Arbitrary name of the user (original input). - NameSanitized string // Sanitized version of the name. -} - // BlockRecordFile is the metadata of a file published on the blockchain type BlockRecordFile struct { Hash []byte // Hash of the file data @@ -73,7 +66,7 @@ type BlockRecordFile struct { // BlockRecordFileTag provides additional metadata about the file. // New tags can be defines in the future without breaking support. type BlockRecordFileTag struct { - Type uint16 // Type (low-level). See TagTypeX constants. + Type uint16 // See TagTypeX constants. Data []byte // Actual data of the tag, decoded into FileTagX structures. // If top bit of Type is set, then Data must be 2, 4, or 8 bytes representing the distance number (positive or negative) of raw record in the block that will be used as data. @@ -111,31 +104,13 @@ type FileTagCreated struct { // ---- low-level encoding ---- -// decodeBlockRecordUser decodes the username, if available. Otherwise return nil. -func decodeBlockRecordUser(recordsRaw []BlockRecordRaw) (user *BlockRecordUser) { - for _, record := range recordsRaw { - if record.Type == RecordTypeUsername { - user = &BlockRecordUser{Name: string(record.Data), NameSanitized: sanitize.Username(string(record.Data))} - // continue to seek for an overriding username record, even though that would be stupid within the same block - } - } - return -} - -// encodeBlockRecordUser encodes the username -func encodeBlockRecordUser(user BlockRecordUser) (recordsRaw []BlockRecordRaw, err error) { - recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeUsername, Data: []byte(user.Name)}) - - return recordsRaw, nil -} - -// decodeBlockRecordFiles decodes only file records. Other records are ignored +// decodeBlockRecordFiles decodes only file records. Other records are ignored. func decodeBlockRecordFiles(recordsRaw []BlockRecordRaw) (files []BlockRecordFile, err error) { for i, record := range recordsRaw { switch record.Type { case RecordTypeFile: if len(record.Data) < 61 { - return nil, errors.New("decodeBlockRecordFiles file record invalid size") + return nil, errors.New("file record invalid size") } file := BlockRecordFile{} @@ -152,7 +127,7 @@ func decodeBlockRecordFiles(recordsRaw []BlockRecordRaw) (files []BlockRecordFil for n := uint16(0); n < countTags; n++ { if index+6 > len(record.Data) { - return nil, errors.New("decodeBlockRecordFiles file record tags invalid size") + return nil, errors.New("file record tags invalid size") } tag := BlockRecordFileTag{} @@ -161,7 +136,7 @@ func decodeBlockRecordFiles(recordsRaw []BlockRecordRaw) (files []BlockRecordFil isDataReference := record.Data[index+1]&0x80 != 0 if index+6+int(tagSize) > len(record.Data) { - return nil, errors.New("decodeBlockRecordFiles file record tag data invalid size") + return nil, errors.New("file record tag data invalid size") } if isDataReference { // reference to RecordTypeTagData record? @@ -173,13 +148,13 @@ func decodeBlockRecordFiles(recordsRaw []BlockRecordRaw) (files []BlockRecordFil } else if tagSize == 8 { refRecordNumber = i + int(int64(binary.LittleEndian.Uint64(record.Data[index+6:index+6+8]))) } else { - return nil, errors.New("decodeBlockRecordFiles file record tag reference invalid size") + return nil, errors.New("file record tag reference invalid size") } if refRecordNumber < 0 || refRecordNumber >= len(recordsRaw) { - return nil, errors.New("decodeBlockRecordFiles file record tag reference not available") + return nil, errors.New("file record tag reference not available") } else if recordsRaw[refRecordNumber].Type != RecordTypeTagData { - return nil, errors.New("decodeBlockRecordFiles file record tag reference invalid") + return nil, errors.New("file record tag reference invalid") } tag.Data = recordsRaw[refRecordNumber].Data @@ -220,7 +195,7 @@ func decodeFileTag(tag BlockRecordFileTag) (decoded interface{}, err error) { case TagTypeDateCreated: if len(tag.Data) != 8 { - return nil, errors.New("decodeFileTag invalid date tag size") + return nil, errors.New("file tag date invalid size") } timeB := int64(binary.LittleEndian.Uint64(tag.Data[0:8])) @@ -369,9 +344,164 @@ func decodeBlockRecords(block *Block) (decoded *BlockDecoded, err error) { decoded.RecordsDecoded = append(decoded.RecordsDecoded, file) } - if user := decodeBlockRecordUser(block.RecordsRaw); user != nil { - decoded.RecordsDecoded = append(decoded.RecordsDecoded, user) + if profile, err := decodeBlockRecordProfile(block.RecordsRaw); err != nil { + return nil, err + } else if profile != nil { + decoded.RecordsDecoded = append(decoded.RecordsDecoded, *profile) } return decoded, nil } + +// ---- Profile data ---- + +// BlockRecordProfile provides information about the end user +// Multiple profile records extend each others field, or overwrite the field. +type BlockRecordProfile struct { + Fields []BlockRecordProfileField // All fields + Blobs []BlockRecordProfileBlob // Blobs +} + +// BlockRecordProfileField contains a single information about the end user. The data is always UTF8 text encoded. +// Note that all profile data is arbitrary and shall be considered untrusted and unverified. +// To establish trust, the user must load Certificates into the blockchain that validate certain data. +type BlockRecordProfileField struct { + Type uint16 // See ProfileFieldX constants. + Text string // The data +} + +// ProfileFieldX constants define well known profile information +const ( + ProfileFieldName = 0 // Arbitrary username + ProfileFieldEmail = 1 // Email address + ProfileFieldWebsite = 2 // Website address + ProfileFieldTwitter = 3 // Twitter account without the @ + ProfileFieldYouTube = 4 // YouTube channel URL + ProfileFieldAddress = 5 // Physical address +) + +type BlockRecordProfileBlob struct { + Type uint16 // See ProfileBlobX constants. + Data []byte // The data +} + +// ProfileBlobX constants define well known blobs +// Pictures should be in JPEG or PNG format. +const ( + ProfileBlobPicture = 0 // Profile picture, unspecified size +) + +// decodeBlockRecordProfile decodes only profile records. Other records are ignored. +func decodeBlockRecordProfile(recordsRaw []BlockRecordRaw) (profile *BlockRecordProfile, err error) { + fields := make(map[uint16]string) + blobs := make(map[uint16][]byte) + + for _, record := range recordsRaw { + if record.Type != RecordTypeProfile { + continue + } + + // header: 4 bytes + if len(record.Data) < 4 { + return nil, errors.New("profile record invalid size") + } + countFields := binary.LittleEndian.Uint16(record.Data[0:2]) + countBlobs := binary.LittleEndian.Uint16(record.Data[2:4]) + + index := 4 + + for n := 0; n < int(countFields); n++ { + if index+4 > len(record.Data) { + return nil, errors.New("profile record field invalid size") + } + + fieldType := binary.LittleEndian.Uint16(record.Data[index : index+2]) + fieldSize := binary.LittleEndian.Uint32(record.Data[index+2 : index+2+4]) + + if index+6+int(fieldSize) > len(record.Data) { + return nil, errors.New("profile record field data invalid size") + } + + fields[fieldType] = string(record.Data[index+6 : index+6+int(fieldSize)]) + + index += 6 + int(fieldSize) + } + + for n := 0; n < int(countBlobs); n++ { + if index+4 > len(record.Data) { + return nil, errors.New("profile record field invalid size") + } + + blobType := binary.LittleEndian.Uint16(record.Data[index : index+2]) + blobSize := binary.LittleEndian.Uint32(record.Data[index+2 : index+2+4]) + + if index+6+int(blobSize) > len(record.Data) { + return nil, errors.New("profile record field data invalid size") + } + + blobs[blobType] = record.Data[index+6 : index+6+int(blobSize)] + + index += 6 + int(blobSize) + } + } + + if len(fields) == 0 && len(blobs) == 0 { + return nil, nil + } + + profile = &BlockRecordProfile{} + + for fieldType, fieldText := range fields { + profile.Fields = append(profile.Fields, BlockRecordProfileField{Type: fieldType, Text: fieldText}) + } + + for blobType, blobData := range blobs { + profile.Blobs = append(profile.Blobs, BlockRecordProfileBlob{Type: blobType, Data: blobData}) + } + + return profile, nil +} + +// encodeBlockRecordProfile encodes the profile record. +func encodeBlockRecordProfile(profile BlockRecordProfile) (recordsRaw []BlockRecordRaw, err error) { + if len(profile.Fields) > math.MaxUint16 || len(profile.Blobs) > math.MaxUint16 { + return nil, errors.New("exceeding max count of fields") + } + + data := make([]byte, 4) + + binary.LittleEndian.PutUint16(data[0:2], uint16(len(profile.Fields))) + binary.LittleEndian.PutUint16(data[2:4], uint16(len(profile.Blobs))) + + for n := range profile.Fields { + storeB := []byte(profile.Fields[n].Text) + + if len(storeB) > math.MaxUint32 { + return nil, errors.New("exceeding max field size") + } + + var tempData [6]byte + binary.LittleEndian.PutUint16(tempData[0:2], profile.Fields[n].Type) + binary.LittleEndian.PutUint32(tempData[2:6], uint32(len(storeB))) + + data = append(data, tempData[:]...) + data = append(data, storeB...) + } + + for n := range profile.Blobs { + if len(profile.Blobs[n].Data) > math.MaxUint32 { + return nil, errors.New("exceeding max blob size") + } + + var tempData [6]byte + binary.LittleEndian.PutUint16(tempData[0:2], profile.Blobs[n].Type) + binary.LittleEndian.PutUint32(tempData[2:6], uint32(len(profile.Blobs[n].Data))) + + data = append(data, tempData[:]...) + data = append(data, profile.Blobs[n].Data...) + } + + recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeProfile, Data: data}) + + return recordsRaw, nil +} diff --git a/Test_test.go b/Test_test.go index d8506bf..a9c6304 100644 --- a/Test_test.go +++ b/Test_test.go @@ -91,7 +91,7 @@ func TestBlockEncoding(t *testing.T) { return } - encoded1, _ := encodeBlockRecordUser(BlockRecordUser{Name: "Test User 1"}) + encoded1, _ := encodeBlockRecordProfile(BlockRecordProfile{Fields: []BlockRecordProfileField{{Type: ProfileFieldName, Text: "Test User 1"}}}) file1 := BlockRecordFile{Hash: hashData([]byte("Test data")), Type: TypeText, Format: FormatText, Size: 9, ID: uuid.New()} file1.TagsDecoded = append(file1.TagsDecoded, FileTagName{Name: "Filename 1.txt"}) @@ -129,7 +129,9 @@ func TestBlockEncoding(t *testing.T) { fmt.Printf("Block details:\n----------------\nNumber: %d\nVersion: %d\nLast Hash: %s\nPublic Key: %s\n", block.Number, block.BlockchainVersion, hex.EncodeToString(block.LastBlockHash), hex.EncodeToString(block.OwnerPublicKey.SerializeCompressed())) for _, decodedR := range decoded.RecordsDecoded { - if file, ok := decodedR.(BlockRecordFile); ok { + switch record := decodedR.(type) { + case BlockRecordFile: + file := record fmt.Printf("* File %s\n", file.ID.String()) fmt.Printf(" Size %d\n", file.Size) fmt.Printf(" Type %d\n", file.Type) @@ -144,7 +146,16 @@ func TestBlockEncoding(t *testing.T) { fmt.Printf(" Folder %s\n", v.Name) } } + + case BlockRecordProfile: + profile := record + fmt.Printf("* Profile\n") + for _, field := range profile.Fields { + fmt.Printf(" %d = %s\n", field.Type, field.Text) + } + } + } }