mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-17 02:47:51 +01:00
Rewrite of profile records on blockchain. This allows to add/update profile data (including future fields and blobs) at any time.
This commit is contained in:
204
Block Records.go
204
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
|
||||
}
|
||||
|
||||
15
Test_test.go
15
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user