mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-19 03:27:50 +01:00
Major refactoring of blockchain code into new sub-package.
This commit is contained in:
168
blockchain/Block Encoding.go
Normal file
168
blockchain/Block Encoding.go
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
File Name: Block Encoding.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
Encoding of a block (it is the same stored in the database and shared in a message):
|
||||
Offset Size Info
|
||||
0 65 Signature of entire block
|
||||
65 32 Hash (blake3) of last block. 0 for first one.
|
||||
97 8 Blockchain version number
|
||||
105 4 Block number
|
||||
109 4 Size of entire block including this header
|
||||
113 2 Count of records that follow
|
||||
|
||||
Each record inside the block has this basic structure:
|
||||
Offset Size Info
|
||||
0 1 Record type
|
||||
1 8 Date created. This remains the same in case of block refactoring.
|
||||
9 4 Size of data
|
||||
13 ? Data (encoding depends on record type)
|
||||
|
||||
*/
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
)
|
||||
|
||||
// Block is a single block containing a set of records (metadata).
|
||||
// It has no upper size limit, although a soft limit of 64 KB - overhead is encouraged for efficiency.
|
||||
type Block struct {
|
||||
OwnerPublicKey *btcec.PublicKey // Owner Public Key, ECDSA (secp256k1) 257-bit
|
||||
NodeID []byte // Node ID of the owner (derived from the public key)
|
||||
LastBlockHash []byte // Hash of the last block. Blake3.
|
||||
BlockchainVersion uint64 // Blockchain version
|
||||
Number uint64 // Block number
|
||||
RecordsRaw []BlockRecordRaw // Block records raw
|
||||
}
|
||||
|
||||
// BlockRecordRaw is a single block record (not decoded)
|
||||
type BlockRecordRaw struct {
|
||||
Type uint8 // Record Type. See RecordTypeX.
|
||||
Date time.Time // Date created. This remains the same in case of block refactoring.
|
||||
Data []byte // Data according to the type
|
||||
}
|
||||
|
||||
const blockHeaderSize = 115
|
||||
const blockRecordHeaderSize = 13
|
||||
|
||||
// decodeBlock decodes a single block
|
||||
func decodeBlock(raw []byte) (block *Block, err error) {
|
||||
if len(raw) < blockHeaderSize {
|
||||
return nil, errors.New("decodeBlock invalid block size")
|
||||
}
|
||||
|
||||
block = &Block{}
|
||||
|
||||
signature := raw[0 : 0+65]
|
||||
|
||||
block.OwnerPublicKey, _, err = btcec.RecoverCompact(btcec.S256(), signature, HashFunction(raw[65:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block.NodeID = PublicKey2NodeID(block.OwnerPublicKey)
|
||||
|
||||
block.LastBlockHash = make([]byte, hashSize)
|
||||
copy(block.LastBlockHash, raw[65:65+hashSize])
|
||||
|
||||
block.BlockchainVersion = binary.LittleEndian.Uint64(raw[97 : 97+8])
|
||||
block.Number = uint64(binary.LittleEndian.Uint32(raw[105 : 105+4])) // for now 32-bit in protocol
|
||||
|
||||
blockSize := binary.LittleEndian.Uint32(raw[109 : 109+4])
|
||||
if blockSize != uint32(len(raw)) {
|
||||
return nil, errors.New("decodeBlock invalid block size")
|
||||
}
|
||||
|
||||
// decode on a low-level all block records
|
||||
countRecords := binary.LittleEndian.Uint16(raw[113 : 113+2])
|
||||
index := 115
|
||||
|
||||
for n := uint16(0); n < countRecords; n++ {
|
||||
if index+blockRecordHeaderSize > len(raw) {
|
||||
return nil, errors.New("decodeBlock record exceeds block size")
|
||||
}
|
||||
|
||||
recordType := raw[index]
|
||||
recordDate := int64(binary.LittleEndian.Uint64(raw[index+1 : index+9])) // Unix time int64, the number of seconds elapsed since January 1, 1970 UTC
|
||||
recordSize := binary.LittleEndian.Uint32(raw[index+9 : index+9+4])
|
||||
index += blockRecordHeaderSize
|
||||
|
||||
if index+int(recordSize) > len(raw) {
|
||||
return nil, errors.New("decodeBlock record exceeds block size")
|
||||
}
|
||||
|
||||
block.RecordsRaw = append(block.RecordsRaw, BlockRecordRaw{Type: recordType, Data: raw[index : index+int(recordSize)], Date: time.Unix(recordDate, 0)})
|
||||
|
||||
index += int(recordSize)
|
||||
}
|
||||
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func encodeBlock(block *Block, ownerPrivateKey *btcec.PrivateKey) (raw []byte, err error) {
|
||||
var buffer bytes.Buffer
|
||||
buffer.Write(make([]byte, 65)) // Signature, filled at the end
|
||||
|
||||
if block.Number > 0 && len(block.LastBlockHash) != hashSize {
|
||||
return nil, errors.New("encodeBlock invalid last block hash")
|
||||
} else if block.Number == 0 { // Block 0: Empty last hash
|
||||
block.LastBlockHash = make([]byte, 32)
|
||||
}
|
||||
buffer.Write(block.LastBlockHash)
|
||||
|
||||
var temp [8]byte
|
||||
binary.LittleEndian.PutUint64(temp[0:8], block.BlockchainVersion)
|
||||
buffer.Write(temp[:])
|
||||
|
||||
binary.LittleEndian.PutUint32(temp[0:4], uint32(block.Number)) // for now 32-bit in protocol
|
||||
buffer.Write(temp[:4])
|
||||
|
||||
buffer.Write(make([]byte, 4)) // Size of block, filled later
|
||||
buffer.Write(make([]byte, 2)) // Count of records, filled later
|
||||
|
||||
// write all records
|
||||
countRecords := uint16(0)
|
||||
|
||||
for _, record := range block.RecordsRaw {
|
||||
if record.Date == (time.Time{}) { // Always set date if not already set
|
||||
record.Date = time.Now()
|
||||
}
|
||||
|
||||
var tempSize, tempDate [8]byte
|
||||
binary.LittleEndian.PutUint32(tempSize[0:4], uint32(len(record.Data)))
|
||||
binary.LittleEndian.PutUint64(tempDate[0:8], uint64(record.Date.UTC().Unix()))
|
||||
|
||||
buffer.Write([]byte{record.Type}) // Record Type
|
||||
buffer.Write(tempDate[:8]) // Date created
|
||||
buffer.Write(tempSize[:4]) // Size of data
|
||||
buffer.Write(record.Data) // Data
|
||||
|
||||
countRecords++
|
||||
}
|
||||
|
||||
// finalize the block
|
||||
raw = buffer.Bytes()
|
||||
if len(raw) < blockHeaderSize {
|
||||
return nil, errors.New("encodeBlock invalid block size")
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint32(raw[109:109+4], uint32(len(raw))) // Size of block
|
||||
binary.LittleEndian.PutUint16(raw[113:113+2], countRecords) // Count of records
|
||||
|
||||
// signature is last
|
||||
signature, err := btcec.SignCompact(btcec.S256(), ownerPrivateKey, HashFunction(raw[65:]), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(raw[0:0+65], signature)
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
316
blockchain/Block Records.go
Normal file
316
blockchain/Block Records.go
Normal file
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
File Name: Block Encoding.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
This files defines the encoding of blocks and records within.
|
||||
|
||||
File records:
|
||||
Offset Size Info
|
||||
0 32 Hash blake3 of the file content
|
||||
32 16 File ID
|
||||
48 1 File Type
|
||||
49 2 File Format
|
||||
51 8 File Size
|
||||
59 2 Count of Tags
|
||||
61 ? Tags
|
||||
|
||||
Each file tag provides additional optional information:
|
||||
Offset Size Info
|
||||
0 2 Type
|
||||
2 4 Size of data that follows
|
||||
6 ? Data according to the tag type
|
||||
|
||||
Tag data record contains only raw data and may be referenced by Tags in File records.
|
||||
This is a basic embedded way of compression when tags are repetitive in multiple files within the same block.
|
||||
|
||||
Profile records:
|
||||
Offset Size Info
|
||||
0 2 Type
|
||||
2 ? Data according to the type
|
||||
|
||||
*/
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ---- Block record structures (decoded) ----
|
||||
|
||||
// RecordTypeX defines the type of the record
|
||||
const (
|
||||
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
|
||||
RecordTypeInvalid1 = 3 // Do not use.
|
||||
RecordTypeCertificate = 4 // Certificate to certify provided information in the blockchain issued by a trusted 3rd party.
|
||||
RecordTypeContentRating = 5 // Content rating (positive).
|
||||
RecordTypeContentReport = 6 // Content report (negative).
|
||||
)
|
||||
|
||||
// BlockRecordFile is the metadata of a file published on the blockchain
|
||||
type BlockRecordFile struct {
|
||||
Hash []byte // Hash of the file data
|
||||
ID uuid.UUID // ID of the file
|
||||
Type uint8 // File Type
|
||||
Format uint16 // File Format
|
||||
Size uint64 // Size of the file data
|
||||
NodeID []byte // Node ID, owner of the file
|
||||
Tags []BlockRecordFileTag // Tags provide additional metadata
|
||||
}
|
||||
|
||||
// BlockRecordFileTag provides metadata about the file.
|
||||
type BlockRecordFileTag struct {
|
||||
Type uint16 // See TagX constants.
|
||||
Data []byte // Data
|
||||
|
||||
// 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.
|
||||
// This is an embedded basic compression algorithm for repetitive tag. For example directory tags or album tags might be heavily repetitive among files.
|
||||
}
|
||||
|
||||
// ---- low-level encoding ----
|
||||
|
||||
// decodeBlockRecordFiles decodes only file records. Other records are ignored.
|
||||
func decodeBlockRecordFiles(recordsRaw []BlockRecordRaw, nodeID []byte) (files []BlockRecordFile, err error) {
|
||||
for i, record := range recordsRaw {
|
||||
switch record.Type {
|
||||
case RecordTypeFile:
|
||||
if len(record.Data) < 61 {
|
||||
return nil, errors.New("file record invalid size")
|
||||
}
|
||||
|
||||
file := BlockRecordFile{NodeID: nodeID}
|
||||
file.Hash = make([]byte, hashSize)
|
||||
copy(file.Hash, record.Data[0:0+hashSize])
|
||||
copy(file.ID[:], record.Data[32:32+16])
|
||||
file.Type = record.Data[48]
|
||||
file.Format = binary.LittleEndian.Uint16(record.Data[49 : 49+2])
|
||||
file.Size = binary.LittleEndian.Uint64(record.Data[51 : 51+8])
|
||||
|
||||
countTags := binary.LittleEndian.Uint16(record.Data[59 : 59+2])
|
||||
|
||||
index := 61
|
||||
|
||||
for n := uint16(0); n < countTags; n++ {
|
||||
if index+6 > len(record.Data) {
|
||||
return nil, errors.New("file record tags invalid size")
|
||||
}
|
||||
|
||||
tag := BlockRecordFileTag{}
|
||||
tag.Type = binary.LittleEndian.Uint16(record.Data[index:index+2]) & 0x7FFF
|
||||
tagSize := binary.LittleEndian.Uint32(record.Data[index+2 : index+2+4])
|
||||
isDataReference := record.Data[index+1]&0x80 != 0
|
||||
|
||||
if index+6+int(tagSize) > len(record.Data) {
|
||||
return nil, errors.New("file record tag data invalid size")
|
||||
}
|
||||
|
||||
if isDataReference { // reference to RecordTypeTagData record?
|
||||
var refRecordNumber int
|
||||
if tagSize == 2 {
|
||||
refRecordNumber = i + int(int16(binary.LittleEndian.Uint16(record.Data[index+6:index+6+2])))
|
||||
} else if tagSize == 4 {
|
||||
refRecordNumber = i + int(int32(binary.LittleEndian.Uint32(record.Data[index+6:index+6+4])))
|
||||
} else if tagSize == 8 {
|
||||
refRecordNumber = i + int(int64(binary.LittleEndian.Uint64(record.Data[index+6:index+6+8])))
|
||||
} else {
|
||||
return nil, errors.New("file record tag reference invalid size")
|
||||
}
|
||||
|
||||
if refRecordNumber < 0 || refRecordNumber >= len(recordsRaw) {
|
||||
return nil, errors.New("file record tag reference not available")
|
||||
} else if recordsRaw[refRecordNumber].Type != RecordTypeTagData {
|
||||
return nil, errors.New("file record tag reference invalid")
|
||||
}
|
||||
|
||||
tag.Data = recordsRaw[refRecordNumber].Data
|
||||
|
||||
} else {
|
||||
tag.Data = record.Data[index+6 : index+6+int(tagSize)]
|
||||
}
|
||||
|
||||
file.Tags = append(file.Tags, tag)
|
||||
|
||||
index += 6 + int(tagSize)
|
||||
}
|
||||
|
||||
file.Tags = append(file.Tags, TagFromDate(TagDateShared, record.Date))
|
||||
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
return files, err
|
||||
}
|
||||
|
||||
// encodeBlockRecordFiles encodes files into the block record data
|
||||
// This function should be called grouped with all files in the same folder. The folder name is deduplicated; only unique folder records will be returned.
|
||||
// Note that this function only stores the folder names as tags; it does not create separate TypeFolder file records.
|
||||
func encodeBlockRecordFiles(files []BlockRecordFile) (recordsRaw []BlockRecordRaw, err error) {
|
||||
uniqueTagDataMap := make(map[string]struct{})
|
||||
duplicateTagDataMap := make(map[string]int) // list of tag data that appeared twice. Number in recordsRaw.
|
||||
|
||||
// loop through all tags to encode them and create list of duplicates that will be replaced by references
|
||||
for n := range files {
|
||||
for _, tag := range files[n].Tags {
|
||||
if len(tag.Data) > 4 {
|
||||
if _, ok := uniqueTagDataMap[string(tag.Data)]; !ok {
|
||||
uniqueTagDataMap[string(tag.Data)] = struct{}{}
|
||||
} else if _, ok := duplicateTagDataMap[string(tag.Data)]; !ok {
|
||||
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeTagData, Data: tag.Data})
|
||||
duplicateTagDataMap[string(tag.Data)] = len(recordsRaw) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// then encode all files as records
|
||||
for n := range files {
|
||||
data := make([]byte, 61)
|
||||
|
||||
if len(files[n].Hash) != hashSize {
|
||||
return nil, errors.New("encodeBlockRecords invalid file hash")
|
||||
}
|
||||
|
||||
copy(data[0:32], files[n].Hash[0:32])
|
||||
copy(data[32:32+16], files[n].ID[:])
|
||||
|
||||
data[48] = files[n].Type
|
||||
binary.LittleEndian.PutUint16(data[49:49+2], files[n].Format)
|
||||
binary.LittleEndian.PutUint64(data[51:51+8], files[n].Size)
|
||||
binary.LittleEndian.PutUint16(data[59:59+2], uint16(len(files[n].Tags)))
|
||||
|
||||
for _, tag := range files[n].Tags {
|
||||
// Some tags are virtual and never stored on the blockchain. If attempted to write, ignore.
|
||||
if tag.IsVirtual() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(tag.Data) > 4 {
|
||||
if refNumber, ok := duplicateTagDataMap[string(tag.Data)]; ok {
|
||||
// In case the data is duplicated, use reference to the RecordTypeTagData instead
|
||||
tag.Type |= 0x8000
|
||||
tag.Data = intToBytes(-(len(recordsRaw) - refNumber))
|
||||
}
|
||||
}
|
||||
|
||||
var tempTag [6]byte
|
||||
|
||||
binary.LittleEndian.PutUint16(tempTag[0:2], tag.Type)
|
||||
binary.LittleEndian.PutUint32(tempTag[2:2+4], uint32(len(tag.Data)))
|
||||
|
||||
data = append(data, tempTag[:]...)
|
||||
data = append(data, tag.Data...)
|
||||
}
|
||||
|
||||
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeFile, Data: data})
|
||||
}
|
||||
|
||||
return recordsRaw, nil
|
||||
}
|
||||
|
||||
// intToBytes encodes int to little endian byte array as it fits to 16, 32 or 64 bit.
|
||||
func intToBytes(number int) (buffer []byte) {
|
||||
buffer = make([]byte, 4)
|
||||
|
||||
if number <= math.MaxInt16 && number >= math.MinInt16 {
|
||||
binary.LittleEndian.PutUint16(buffer[0:2], uint16(number))
|
||||
return buffer[0:2]
|
||||
} else if number <= math.MaxInt32 && number >= math.MinInt32 {
|
||||
binary.LittleEndian.PutUint32(buffer[0:4], uint32(number))
|
||||
return buffer[0:4]
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint64(buffer[0:8], uint64(number))
|
||||
return buffer[0:8]
|
||||
}
|
||||
|
||||
// ---- high-level decoding ----
|
||||
|
||||
// BlockDecoded contains the decoded records from a block
|
||||
type BlockDecoded struct {
|
||||
Block
|
||||
RecordsDecoded []interface{} // Decoded records. See BlockRecordX structures.
|
||||
}
|
||||
|
||||
// decodeBlockRecords decodes all raw records in the block and returns a high-level decoded structure
|
||||
// Use decodeBlockRecordX instead for specific record decoding.
|
||||
func decodeBlockRecords(block *Block) (decoded *BlockDecoded, err error) {
|
||||
decoded = &BlockDecoded{Block: *block}
|
||||
|
||||
files, err := decodeBlockRecordFiles(block.RecordsRaw, block.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
decoded.RecordsDecoded = append(decoded.RecordsDecoded, file)
|
||||
}
|
||||
|
||||
if profileFields, err := decodeBlockRecordProfile(block.RecordsRaw); err != nil {
|
||||
return nil, err
|
||||
} else if len(profileFields) > 0 {
|
||||
decoded.RecordsDecoded = append(decoded.RecordsDecoded, profileFields)
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// ---- Profile data ----
|
||||
|
||||
// BlockRecordProfile provides information about the end user.
|
||||
type BlockRecordProfile struct {
|
||||
Type uint16 // See ProfileX constants.
|
||||
Data []byte // Data
|
||||
}
|
||||
|
||||
// decodeBlockRecordProfile decodes only profile records. Other records are ignored.
|
||||
func decodeBlockRecordProfile(recordsRaw []BlockRecordRaw) (fields []BlockRecordProfile, err error) {
|
||||
fieldMap := make(map[uint16][]byte)
|
||||
|
||||
for _, record := range recordsRaw {
|
||||
if record.Type != RecordTypeProfile {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(record.Data) < 2 {
|
||||
return nil, errors.New("profile record invalid size")
|
||||
}
|
||||
|
||||
fieldType := binary.LittleEndian.Uint16(record.Data[0:2])
|
||||
fieldMap[fieldType] = record.Data[2:]
|
||||
}
|
||||
|
||||
for fieldType, fieldData := range fieldMap {
|
||||
fields = append(fields, BlockRecordProfile{Type: fieldType, Data: fieldData})
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// encodeBlockRecordProfile encodes the profile record.
|
||||
func encodeBlockRecordProfile(fields []BlockRecordProfile) (recordsRaw []BlockRecordRaw, err error) {
|
||||
if len(fields) > math.MaxUint16 {
|
||||
return nil, errors.New("exceeding max count of fields")
|
||||
}
|
||||
|
||||
for n := range fields {
|
||||
if len(fields[n].Data) > math.MaxUint32 {
|
||||
return nil, errors.New("exceeding max field size")
|
||||
}
|
||||
|
||||
data := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(data[0:2], fields[n].Type)
|
||||
data = append(data, fields[n].Data...)
|
||||
|
||||
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeProfile, Data: data})
|
||||
}
|
||||
|
||||
return recordsRaw, nil
|
||||
}
|
||||
471
blockchain/Blockchain.go
Normal file
471
blockchain/Blockchain.go
Normal file
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
File Name: Blockchain.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
All blocks and the blockchain header are stored in a key/value database.
|
||||
The key for the blockchain header is keyHeader and for each block is the block number as 64-bit unsigned integer little endian.
|
||||
|
||||
Encoding of the blockchain header:
|
||||
Offset Size Info
|
||||
0 8 Height of the blockchain
|
||||
8 8 Version of the blockchain
|
||||
16 2 Format of the blockchain. This provides backward compatibility.
|
||||
18 65 Signature
|
||||
|
||||
*/
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/PeernetOfficial/core/store"
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Blockchain stores the blockchain's header in memory. Any changes must be synced to disk!
|
||||
type Blockchain struct {
|
||||
// header
|
||||
height uint64 // Height is exchanged as uint32 in the protocol, but stored as uint64.
|
||||
version uint64 // Version is always uint64.
|
||||
format uint16 // Format is only locally used.
|
||||
|
||||
// internals
|
||||
publicKey *btcec.PublicKey // Public Key of the owner. This must match the ones used on disk.
|
||||
privateKey *btcec.PrivateKey // Private Key of the owner. This must match the ones used on disk.
|
||||
path string // Path of the blockchain on disk. Depends on key-value store whether a filename or folder.
|
||||
database store.Store // The database storing the blockchain.
|
||||
sync.Mutex // synchronized access to the header
|
||||
}
|
||||
|
||||
// HashFunction must be set by the caller to the hash function (blake3) that shall be used.
|
||||
var HashFunction func(data []byte) (hash []byte)
|
||||
|
||||
// hashSize is the blake3 digest size in bytes
|
||||
const hashSize = 32
|
||||
|
||||
// PublicKey2NodeID must be set by the caller to the function generating the node ID from the public key. Typically it would use the HashFunction.
|
||||
var PublicKey2NodeID func(publicKey *btcec.PublicKey) (nodeID []byte)
|
||||
|
||||
// Init initializes the given blockchain. It creates the blockchain file if it does not exist already.
|
||||
func Init(privateKey *btcec.PrivateKey, path string) (blockchain *Blockchain, err error) {
|
||||
blockchain = &Blockchain{privateKey: privateKey, path: path}
|
||||
publicKey := privateKey.PubKey()
|
||||
|
||||
// open existing blockchain file or create new one
|
||||
if blockchain.database, err = store.NewPogrebStore(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify header
|
||||
var found bool
|
||||
|
||||
found, err = blockchain.headerRead()
|
||||
if err != nil {
|
||||
return blockchain, err // likely corrupt blockchain database
|
||||
} else if !found {
|
||||
// First run: create header signature!
|
||||
blockchain.publicKey = publicKey
|
||||
|
||||
if err := blockchain.headerWrite(0, 0); err != nil {
|
||||
return blockchain, err
|
||||
}
|
||||
} else if !blockchain.publicKey.IsEqual(publicKey) {
|
||||
return blockchain, errors.New("corrupt user blockchain database. Public key mismatch")
|
||||
}
|
||||
|
||||
return blockchain, nil
|
||||
}
|
||||
|
||||
// the key names in the key/value database are constant and must not collide with block numbers (i.e. they must be >64 bit)
|
||||
const keyHeader = "header blockchain"
|
||||
|
||||
// headerRead reads the header from the blockchain and decodes it.
|
||||
func (blockchain *Blockchain) headerRead() (found bool, err error) {
|
||||
buffer, found := blockchain.database.Get([]byte(keyHeader))
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(buffer) != 83 {
|
||||
return true, errors.New("blockchain header size mismatch")
|
||||
}
|
||||
|
||||
blockchain.height = binary.LittleEndian.Uint64(buffer[0:8])
|
||||
blockchain.version = binary.LittleEndian.Uint64(buffer[8:16])
|
||||
blockchain.format = binary.LittleEndian.Uint16(buffer[16:18])
|
||||
signature := buffer[18 : 18+65]
|
||||
|
||||
if blockchain.format != 0 {
|
||||
return true, errors.New("future blockchain format not supported. You must go back to the future!")
|
||||
}
|
||||
|
||||
blockchain.publicKey, _, err = btcec.RecoverCompact(btcec.S256(), signature, HashFunction(buffer[0:18]))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// headerWrite writes the header to the blockchain and signs it.
|
||||
func (blockchain *Blockchain) headerWrite(height, version uint64) (err error) {
|
||||
blockchain.height = height
|
||||
blockchain.version = version
|
||||
|
||||
var buffer [83]byte
|
||||
binary.LittleEndian.PutUint64(buffer[0:8], height)
|
||||
binary.LittleEndian.PutUint64(buffer[8:16], version)
|
||||
binary.LittleEndian.PutUint16(buffer[16:18], 0) // Current format is 0
|
||||
|
||||
signature, err := btcec.SignCompact(btcec.S256(), blockchain.privateKey, HashFunction(buffer[0:18]), true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(signature) != 65 {
|
||||
return errors.New("signature length invalid")
|
||||
}
|
||||
|
||||
copy(buffer[18:18+65], signature)
|
||||
|
||||
err = blockchain.database.Set([]byte(keyHeader), buffer[:])
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// BlockchainStatusX provides information about the blockchain status. Some errors codes indicate a corruption.
|
||||
const (
|
||||
BlockchainStatusOK = 0 // No problems in the blockchain detected.
|
||||
BlockchainStatusBlockNotFound = 1 // Missing block in the blockchain.
|
||||
BlockchainStatusCorruptBlock = 2 // Error block encoding
|
||||
BlockchainStatusCorruptBlockRecord = 3 // Error block record encoding
|
||||
BlockchainStatusDataNotFound = 4 // Requested data not available in the blockchain
|
||||
)
|
||||
|
||||
// blockNumberToKey returns the database key for the given block number
|
||||
func blockNumberToKey(number uint64) (key []byte) {
|
||||
var target [8]byte
|
||||
binary.LittleEndian.PutUint64(target[:], number)
|
||||
|
||||
return target[:]
|
||||
}
|
||||
|
||||
// Iterate iterates over the blockchain. Status is BlockchainStatusX.
|
||||
// If the callback returns non-zero, the function aborts and returns the inner status code.
|
||||
func (blockchain *Blockchain) Iterate(callback func(block *Block) int) (status int) {
|
||||
// read all blocks until height is reached
|
||||
height := blockchain.height
|
||||
|
||||
for blockN := uint64(0); blockN < height; blockN++ {
|
||||
blockRaw, found := blockchain.database.Get(blockNumberToKey(blockN))
|
||||
if !found || len(blockRaw) == 0 {
|
||||
return BlockchainStatusBlockNotFound
|
||||
}
|
||||
|
||||
block, err := decodeBlock(blockRaw)
|
||||
if err != nil {
|
||||
return BlockchainStatusCorruptBlock
|
||||
}
|
||||
|
||||
if statusI := callback(block); statusI != BlockchainStatusOK {
|
||||
return statusI
|
||||
}
|
||||
}
|
||||
|
||||
return BlockchainStatusOK
|
||||
}
|
||||
|
||||
// IterateDeleteRecord iterates over the blockchain to find records to delete. Status is BlockchainStatusX.
|
||||
// deleteAction is 0 = no action on record, 1 = delete record, 2 = replace record, 3 = error blockchain corrupt
|
||||
// If the callback returns true, the record will be deleted. The blockchain will be automatically refactored and height and version updated.
|
||||
func (blockchain *Blockchain) IterateDeleteRecord(callback func(record *BlockRecordRaw) (deleteAction int)) (newHeight, newVersion uint64, status int) {
|
||||
blockchain.Lock()
|
||||
defer blockchain.Unlock()
|
||||
|
||||
// New blockchain keeps track of the new blocks. If anything changes in the blockchain, it must be recalculated and the version number increased.
|
||||
var blockchainNew []Block
|
||||
refactorBlockchain := false
|
||||
refactorVersion := blockchain.version + 1
|
||||
|
||||
// Read all blocks until height is reached. At the end the height and version might be different if blocks are deleted.
|
||||
height := blockchain.height
|
||||
|
||||
for blockN := uint64(0); blockN < height; blockN++ {
|
||||
blockRaw, found := blockchain.database.Get(blockNumberToKey(blockN))
|
||||
if !found || len(blockRaw) == 0 {
|
||||
return 0, 0, BlockchainStatusBlockNotFound
|
||||
}
|
||||
|
||||
block, err := decodeBlock(blockRaw)
|
||||
if err != nil {
|
||||
return 0, 0, BlockchainStatusCorruptBlock
|
||||
}
|
||||
|
||||
// loop through all records in this block
|
||||
refactorBlock := false
|
||||
var newRecordsRaw []BlockRecordRaw
|
||||
|
||||
for n := range block.RecordsRaw {
|
||||
switch callback(&block.RecordsRaw[n]) {
|
||||
case 0: // no action on record
|
||||
newRecordsRaw = append(newRecordsRaw, block.RecordsRaw[n])
|
||||
|
||||
case 1: // delete record
|
||||
refactorBlock = true
|
||||
refactorBlockchain = true
|
||||
|
||||
case 2: // replace record
|
||||
newRecordsRaw = append(newRecordsRaw, block.RecordsRaw[n])
|
||||
refactorBlock = true
|
||||
refactorBlockchain = true
|
||||
|
||||
case 3: // error blockchain corrupt
|
||||
return 0, 0, BlockchainStatusCorruptBlockRecord
|
||||
}
|
||||
}
|
||||
|
||||
// If refactor, re-calculate the block. All later blocks need to be re-encoded due to change of previous block hash. The version number needs to change.
|
||||
if refactorBlock {
|
||||
if len(newRecordsRaw) > 0 {
|
||||
blockchainNew = append(blockchainNew, Block{OwnerPublicKey: blockchain.publicKey, RecordsRaw: newRecordsRaw, BlockchainVersion: refactorVersion, Number: uint64(len(blockchainNew))})
|
||||
}
|
||||
} else {
|
||||
blockchainNew = append(blockchainNew, Block{OwnerPublicKey: blockchain.publicKey, RecordsRaw: block.RecordsRaw, BlockchainVersion: refactorVersion, Number: uint64(len(blockchainNew))})
|
||||
}
|
||||
}
|
||||
|
||||
if refactorBlockchain {
|
||||
var lastBlockHash []byte
|
||||
|
||||
for _, block := range blockchainNew {
|
||||
block.LastBlockHash = lastBlockHash
|
||||
|
||||
raw, err := encodeBlock(&block, blockchain.privateKey)
|
||||
if err != nil {
|
||||
return 0, 0, BlockchainStatusCorruptBlock
|
||||
}
|
||||
|
||||
// store the block
|
||||
blockchain.database.Set(blockNumberToKey(block.Number), raw)
|
||||
|
||||
lastBlockHash = HashFunction(raw)
|
||||
}
|
||||
|
||||
// update the blockchain header in the database
|
||||
blockchain.headerWrite(uint64(len(blockchainNew)), refactorVersion)
|
||||
|
||||
// delete orphaned blocks
|
||||
for n := blockchain.height; n < height; n++ {
|
||||
blockchain.database.Delete(blockNumberToKey(n))
|
||||
}
|
||||
}
|
||||
|
||||
return blockchain.height, blockchain.version, BlockchainStatusOK
|
||||
}
|
||||
|
||||
// ---- blockchain manipulation functions ----
|
||||
|
||||
// Header returns the users blockchain header which stores the height and version number.
|
||||
func (blockchain *Blockchain) Header() (publicKey *btcec.PublicKey, height uint64, version uint64) {
|
||||
blockchain.Lock()
|
||||
defer blockchain.Unlock()
|
||||
|
||||
return blockchain.publicKey, blockchain.height, blockchain.version
|
||||
}
|
||||
|
||||
// Append appends a new block to the blockchain based on the provided raw records.
|
||||
// Status: BlockchainStatusX (0-2): 0 = Success, 1 = Error block not found, 2 = Error block encoding
|
||||
func (blockchain *Blockchain) Append(RecordsRaw []BlockRecordRaw) (newHeight, newVersion uint64, status int) {
|
||||
blockchain.Lock()
|
||||
defer blockchain.Unlock()
|
||||
|
||||
block := &Block{OwnerPublicKey: blockchain.publicKey, RecordsRaw: RecordsRaw}
|
||||
|
||||
// set the last block hash first
|
||||
if blockchain.height > 0 {
|
||||
previousBlockRaw, found := blockchain.database.Get(blockNumberToKey(blockchain.height - 1))
|
||||
if !found || len(previousBlockRaw) == 0 {
|
||||
return 0, 0, BlockchainStatusBlockNotFound
|
||||
}
|
||||
|
||||
block.LastBlockHash = HashFunction(previousBlockRaw)
|
||||
}
|
||||
|
||||
block.Number = blockchain.height
|
||||
block.BlockchainVersion = blockchain.version
|
||||
|
||||
raw, err := encodeBlock(block, blockchain.privateKey)
|
||||
if err != nil {
|
||||
return 0, 0, BlockchainStatusCorruptBlock
|
||||
}
|
||||
|
||||
// store the block
|
||||
blockchain.database.Set(blockNumberToKey(block.Number), raw)
|
||||
|
||||
// update the blockchain header in the database, increase blockchain height
|
||||
blockchain.headerWrite(blockchain.height+1, blockchain.version)
|
||||
|
||||
return blockchain.height, blockchain.version, BlockchainStatusOK
|
||||
}
|
||||
|
||||
// Read reads the block number from the blockchain. Status is BlockchainStatusX.
|
||||
func (blockchain *Blockchain) Read(number uint64) (decoded *BlockDecoded, status int, err error) {
|
||||
if number >= blockchain.height {
|
||||
return nil, BlockchainStatusBlockNotFound, errors.New("block number exceeds blockchain height")
|
||||
}
|
||||
|
||||
blockRaw, found := blockchain.database.Get(blockNumberToKey(number))
|
||||
if !found || len(blockRaw) == 0 {
|
||||
return nil, BlockchainStatusBlockNotFound, errors.New("block not found")
|
||||
}
|
||||
|
||||
block, err := decodeBlock(blockRaw)
|
||||
if err != nil {
|
||||
return nil, BlockchainStatusCorruptBlock, err
|
||||
}
|
||||
|
||||
decoded, err = decodeBlockRecords(block)
|
||||
if err != nil {
|
||||
return nil, BlockchainStatusCorruptBlock, err
|
||||
}
|
||||
|
||||
return decoded, BlockchainStatusOK, nil
|
||||
}
|
||||
|
||||
// AddFiles adds files to the blockchain. Status is BlockchainStatusX.
|
||||
// It makes sense to group all files in the same directory into one call, since only one directory record will be created per unique directory per block.
|
||||
func (blockchain *Blockchain) AddFiles(files []BlockRecordFile) (newHeight, newVersion uint64, status int) {
|
||||
encoded, err := encodeBlockRecordFiles(files)
|
||||
if err != nil {
|
||||
return 0, 0, BlockchainStatusCorruptBlockRecord
|
||||
}
|
||||
|
||||
return blockchain.Append(encoded)
|
||||
}
|
||||
|
||||
// ListFiles returns a list of all files. Status is BlockchainStatusX.
|
||||
// If there is a corruption in the blockchain it will stop reading but return the files parsed so far.
|
||||
func (blockchain *Blockchain) ListFiles() (files []BlockRecordFile, status int) {
|
||||
status = blockchain.Iterate(func(block *Block) (statusI int) {
|
||||
filesMore, err := decodeBlockRecordFiles(block.RecordsRaw, block.NodeID)
|
||||
if err != nil {
|
||||
return BlockchainStatusCorruptBlockRecord
|
||||
}
|
||||
files = append(files, filesMore...)
|
||||
|
||||
return BlockchainStatusOK
|
||||
})
|
||||
|
||||
return files, status
|
||||
}
|
||||
|
||||
// ProfileReadField reads the specified profile field. See ProfileX for the list of recognized fields. The encoding depends on the field type. Status is BlockchainStatusX.
|
||||
func (blockchain *Blockchain) ProfileReadField(index uint16) (data []byte, status int) {
|
||||
found := false
|
||||
|
||||
status = blockchain.Iterate(func(block *Block) (statusI int) {
|
||||
fields, err := decodeBlockRecordProfile(block.RecordsRaw)
|
||||
if err != nil {
|
||||
return BlockchainStatusCorruptBlockRecord
|
||||
} else if len(fields) == 0 {
|
||||
return BlockchainStatusOK
|
||||
}
|
||||
|
||||
// Check if the field is available in the profile record. If there are multiple records, only return the latest one.
|
||||
for n := range fields {
|
||||
if fields[n].Type == index {
|
||||
data = fields[n].Data
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
return BlockchainStatusOK
|
||||
})
|
||||
|
||||
if status != BlockchainStatusOK {
|
||||
return nil, status
|
||||
} else if !found {
|
||||
return nil, BlockchainStatusDataNotFound
|
||||
}
|
||||
|
||||
return data, BlockchainStatusOK
|
||||
}
|
||||
|
||||
// ProfileList lists all profile fields. Status is BlockchainStatusX.
|
||||
func (blockchain *Blockchain) ProfileList() (fields []BlockRecordProfile, status int) {
|
||||
uniqueFields := make(map[uint16][]byte)
|
||||
|
||||
status = blockchain.Iterate(func(block *Block) (statusI int) {
|
||||
fields, err := decodeBlockRecordProfile(block.RecordsRaw)
|
||||
if err != nil {
|
||||
return BlockchainStatusCorruptBlockRecord
|
||||
}
|
||||
|
||||
for n := range fields {
|
||||
uniqueFields[fields[n].Type] = fields[n].Data
|
||||
}
|
||||
|
||||
return BlockchainStatusOK
|
||||
})
|
||||
|
||||
for key, value := range uniqueFields {
|
||||
fields = append(fields, BlockRecordProfile{Type: key, Data: value})
|
||||
}
|
||||
|
||||
return fields, status
|
||||
}
|
||||
|
||||
// ProfileWrite writes profile fields and blobs to the blockchain. Status is BlockchainStatusX.
|
||||
func (blockchain *Blockchain) ProfileWrite(fields []BlockRecordProfile) (newHeight, newVersion uint64, status int) {
|
||||
encoded, err := encodeBlockRecordProfile(fields)
|
||||
if err != nil {
|
||||
return 0, 0, BlockchainStatusCorruptBlockRecord
|
||||
}
|
||||
|
||||
return blockchain.Append(encoded)
|
||||
}
|
||||
|
||||
// ProfileDelete deletes fields and blobs from the blockchain. Status is BlockchainStatusX.
|
||||
func (blockchain *Blockchain) ProfileDelete(fields []uint16) (newHeight, newVersion uint64, status int) {
|
||||
return blockchain.IterateDeleteRecord(func(record *BlockRecordRaw) (deleteAction int) {
|
||||
if record.Type != RecordTypeProfile {
|
||||
return 0 // no action
|
||||
}
|
||||
|
||||
existingFields, err := decodeBlockRecordProfile([]BlockRecordRaw{*record})
|
||||
if err != nil || len(existingFields) != 1 {
|
||||
return 3 // error blockchain corrupt
|
||||
}
|
||||
|
||||
for _, i := range fields {
|
||||
if i == existingFields[0].Type { // found a file ID to delete?
|
||||
return 1 // delete record
|
||||
}
|
||||
}
|
||||
|
||||
return 0 // no action on record
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteFiles deletes files from the blockchain. Status is BlockchainStatusX.
|
||||
func (blockchain *Blockchain) DeleteFiles(IDs []uuid.UUID) (newHeight, newVersion uint64, status int) {
|
||||
return blockchain.IterateDeleteRecord(func(record *BlockRecordRaw) (deleteAction int) {
|
||||
if record.Type != RecordTypeFile {
|
||||
return 0 // no action on record
|
||||
}
|
||||
|
||||
filesDecoded, err := decodeBlockRecordFiles([]BlockRecordRaw{*record}, nil)
|
||||
if err != nil || len(filesDecoded) != 1 {
|
||||
return 3 // error blockchain corrupt
|
||||
}
|
||||
|
||||
for _, id := range IDs {
|
||||
if id == filesDecoded[0].ID { // found a file ID to delete?
|
||||
return 1 // delete record
|
||||
}
|
||||
}
|
||||
|
||||
return 0 // no action on record
|
||||
})
|
||||
}
|
||||
104
blockchain/File Tag.go
Normal file
104
blockchain/File Tag.go
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
File Name: File Tag.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
Metadata tags provide meta information about files.
|
||||
*/
|
||||
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// List of defined file tags. Virtual tags are generated at runtime and are read-only. They cannot be stored on the blockchain.
|
||||
const (
|
||||
TagName = 0 // Name of file.
|
||||
TagFolder = 1 // Folder name.
|
||||
TagDescription = 2 // Arbitrary description of the file. May contain hashtags.
|
||||
TagDateShared = 3 // When the file was published on the blockchain. Virtual.
|
||||
TagDateCreated = 4 // Date when the file was originally created. This may differ from the date in the block record, which indicates when the file was shared.
|
||||
TagSharedByCount = 5 // Count of peers that share the file. Virtual.
|
||||
TagSharedByGeoIP = 6 // GeoIP data of peers that are sharing the file. CSV encoded with header "latitude,longitude". Virtual.
|
||||
)
|
||||
|
||||
// Future tags to be defined for audio/video: Artist, Album, Title, Length, Bitrate, Codec
|
||||
// Windows list: https://docs.microsoft.com/en-us/windows/win32/wmdm/metadata-constants
|
||||
|
||||
// ---- encoding ----
|
||||
|
||||
// Date returns the tags data as date encoded
|
||||
func (tag *BlockRecordFileTag) Date() (time.Time, error) {
|
||||
if tag == nil {
|
||||
return time.Time{}, errors.New("tag not available")
|
||||
} else if len(tag.Data) != 8 {
|
||||
return time.Time{}, errors.New("file tag date invalid size")
|
||||
}
|
||||
|
||||
timeB := int64(binary.LittleEndian.Uint64(tag.Data[0:8]))
|
||||
return time.Unix(timeB, 0).UTC(), nil
|
||||
}
|
||||
|
||||
// Text returns the tags data as text encoded
|
||||
func (tag *BlockRecordFileTag) Text() string {
|
||||
return string(tag.Data)
|
||||
}
|
||||
|
||||
// Number returns the tags data as uint64. It returns 0 if the data cannot be decoded.
|
||||
func (tag *BlockRecordFileTag) Number() uint64 {
|
||||
if len(tag.Data) != 8 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint64(tag.Data[0:8])
|
||||
}
|
||||
|
||||
// IsVirtual checks if the tag is virtual.
|
||||
func (tag *BlockRecordFileTag) IsVirtual() bool {
|
||||
return IsTagVirtual(tag.Type)
|
||||
}
|
||||
|
||||
// TagFromDate returns a tag from date
|
||||
func TagFromDate(Type uint16, Date time.Time) BlockRecordFileTag {
|
||||
var tempDate [8]byte
|
||||
binary.LittleEndian.PutUint64(tempDate[0:8], uint64(Date.UTC().Unix()))
|
||||
|
||||
return BlockRecordFileTag{Type: Type, Data: tempDate[:]}
|
||||
}
|
||||
|
||||
// TagFromText returns a tag from text
|
||||
func TagFromText(Type uint16, Text string) BlockRecordFileTag {
|
||||
return BlockRecordFileTag{Type: Type, Data: []byte(Text)}
|
||||
}
|
||||
|
||||
// TagFromNumber returns a tag from a number
|
||||
func TagFromNumber(Type uint16, Number uint64) BlockRecordFileTag {
|
||||
var tempDate [8]byte
|
||||
binary.LittleEndian.PutUint64(tempDate[0:8], Number)
|
||||
|
||||
return BlockRecordFileTag{Type: Type, Data: tempDate[:]}
|
||||
}
|
||||
|
||||
// IsTagVirtual checks if the tag is a virtual one.
|
||||
func IsTagVirtual(Type uint16) bool {
|
||||
switch Type {
|
||||
case TagDateShared, TagSharedByCount, TagSharedByGeoIP:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetTag returns the tag with the type or nil if not available.
|
||||
func (file *BlockRecordFile) GetTag(Type uint16) (tag *BlockRecordFileTag) {
|
||||
for n := range file.Tags {
|
||||
if file.Tags[n].Type == Type {
|
||||
return &file.Tags[n]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
32
blockchain/Profile Data.go
Normal file
32
blockchain/Profile Data.go
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
File Name: Profile Data.go
|
||||
Copyright: 2021 Peernet s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package blockchain
|
||||
|
||||
// List of recognized profile fields.
|
||||
const (
|
||||
ProfileName = 0 // Arbitrary username
|
||||
ProfileEmail = 1 // Email address
|
||||
ProfileWebsite = 2 // Website address
|
||||
ProfileTwitter = 3 // Twitter account without the @
|
||||
ProfileYouTube = 4 // YouTube channel URL
|
||||
ProfileAddress = 5 // Physical address
|
||||
ProfilePicture = 6 // Profile picture, blob
|
||||
)
|
||||
|
||||
// The encoding of profile fields depends on the field. Text data is always UTF-8 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.
|
||||
|
||||
// Text returns the profile field as text encoded
|
||||
func (info *BlockRecordProfile) Text() string {
|
||||
return string(info.Data)
|
||||
}
|
||||
|
||||
// ProfileFieldFromText returns a profile field from text
|
||||
func ProfileFieldFromText(Type uint16, Text string) BlockRecordProfile {
|
||||
return BlockRecordProfile{Type: Type, Data: []byte(Text)}
|
||||
}
|
||||
265
blockchain/Test_test.go
Normal file
265
blockchain/Test_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/google/uuid"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
func TestBlockEncoding(t *testing.T) {
|
||||
initRequiredFunctions()
|
||||
privateKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
encoded1, _ := encodeBlockRecordProfile([]BlockRecordProfile{ProfileFieldFromText(ProfileName, "Test User 1")})
|
||||
|
||||
file1 := BlockRecordFile{Hash: testHashData([]byte("Test data")), Type: testTypeText, Format: testFormatText, Size: 9, ID: uuid.New()}
|
||||
file1.Tags = append(file1.Tags, TagFromText(TagName, "Filename 1.txt"))
|
||||
file1.Tags = append(file1.Tags, TagFromText(TagFolder, "documents\\sub folder"))
|
||||
|
||||
file2 := BlockRecordFile{Hash: testHashData([]byte("Test data 2!")), Type: testTypeText, Format: testFormatText, Size: 10, ID: uuid.New()}
|
||||
file2.Tags = append(file2.Tags, TagFromText(TagName, "Filename 2.txt"))
|
||||
file2.Tags = append(file2.Tags, TagFromText(TagFolder, "documents\\sub folder"))
|
||||
|
||||
encodedFiles, _ := encodeBlockRecordFiles([]BlockRecordFile{file1, file2})
|
||||
|
||||
blockE := &Block{BlockchainVersion: 42, Number: 0}
|
||||
blockE.RecordsRaw = append(blockE.RecordsRaw, encoded1...)
|
||||
blockE.RecordsRaw = append(blockE.RecordsRaw, encodedFiles...)
|
||||
|
||||
raw, err := encodeBlock(blockE, privateKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
block, err := decodeBlock(raw)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
decoded, err := decodeBlockRecords(block)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// output the block details
|
||||
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 {
|
||||
switch record := decodedR.(type) {
|
||||
case BlockRecordFile:
|
||||
printFile(record)
|
||||
|
||||
case BlockRecordProfile:
|
||||
printProfileField(record)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func initRequiredFunctions() {
|
||||
HashFunction = testHashData
|
||||
PublicKey2NodeID = func(publicKey *btcec.PublicKey) (nodeID []byte) {
|
||||
return testHashData(publicKey.SerializeCompressed())
|
||||
}
|
||||
}
|
||||
|
||||
func initTestPrivateKey() (blockchain *Blockchain, err error) {
|
||||
// use static test key, otherwise tests will be inconsistent (would otherwise fail to open blockchain database)
|
||||
privateKeyTestA := "d65da474861d826edd29c1307f1250d79e9dbf84e3a2449022658445c8d8ed63"
|
||||
privateKeyB, _ := hex.DecodeString(privateKeyTestA)
|
||||
peerPrivateKey, peerPublicKey := btcec.PrivKeyFromBytes(btcec.S256(), privateKeyB)
|
||||
|
||||
fmt.Printf("Loaded public key: %s\n", hex.EncodeToString(peerPublicKey.SerializeCompressed()))
|
||||
|
||||
initRequiredFunctions()
|
||||
|
||||
return Init(peerPrivateKey, "test.blockchain")
|
||||
}
|
||||
|
||||
func TestBlockchainAdd(t *testing.T) {
|
||||
blockchain, err := initTestPrivateKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
file1 := BlockRecordFile{Hash: testHashData([]byte("Test data")), Type: testTypeText, Format: testFormatText, Size: 9, ID: uuid.New()}
|
||||
file1.Tags = append(file1.Tags, TagFromText(TagName, "Filename 1.txt"))
|
||||
file1.Tags = append(file1.Tags, TagFromText(TagFolder, "documents\\sub folder"))
|
||||
|
||||
newHeight, newVersion, status := blockchain.AddFiles([]BlockRecordFile{file1})
|
||||
|
||||
switch status {
|
||||
case 0:
|
||||
case 1: // Error previous block not found
|
||||
fmt.Printf("Error adding file to blockchain: Previous block not found.\n")
|
||||
case 2: // Error block encoding
|
||||
fmt.Printf("Error adding file to blockchain: Error block encoding.\n")
|
||||
case 3: // Error block record encoding
|
||||
fmt.Printf("Error adding file to blockchain: Error block record encoding.\n")
|
||||
default:
|
||||
fmt.Printf("Error adding file to blockchain: Unknown status %d\n", status)
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Success adding files to blockchain. New blockchain height %d version %d\n", newHeight, newVersion)
|
||||
}
|
||||
|
||||
func TestBlockchainRead(t *testing.T) {
|
||||
blockchain, err := initTestPrivateKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
blockNumber := uint64(0)
|
||||
|
||||
decoded, status, err := blockchain.Read(blockNumber)
|
||||
switch status {
|
||||
case 0:
|
||||
case 1: // Error block not found
|
||||
fmt.Printf("Error reading block %d: Block not found.\n", blockNumber)
|
||||
case 2: // Error block encoding
|
||||
fmt.Printf("Error reading block %d: Block encoding corrupt: %s\n", blockNumber, err.Error())
|
||||
case 3: // Error block record encoding
|
||||
fmt.Printf("Error reading block %d: Block record encoding corrupt.\n", blockNumber)
|
||||
default:
|
||||
fmt.Printf("Error reading block %d: Unknown status %d\n", blockNumber, status)
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, decodedR := range decoded.RecordsDecoded {
|
||||
if file, ok := decodedR.(BlockRecordFile); ok {
|
||||
printFile(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printFile(file BlockRecordFile) {
|
||||
fmt.Printf("* File %s\n", file.ID.String())
|
||||
fmt.Printf(" Size %d\n", file.Size)
|
||||
fmt.Printf(" Type %d\n", file.Type)
|
||||
fmt.Printf(" Format %d\n", file.Format)
|
||||
fmt.Printf(" Hash %s\n", hex.EncodeToString(file.Hash))
|
||||
|
||||
for _, tag := range file.Tags {
|
||||
switch tag.Type {
|
||||
case TagName:
|
||||
fmt.Printf(" Name %s\n", tag.Text())
|
||||
case TagFolder:
|
||||
fmt.Printf(" Folder %s\n", tag.Text())
|
||||
case TagDescription:
|
||||
fmt.Printf(" Description %s\n", tag.Text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockchainDelete(t *testing.T) {
|
||||
blockchain, err := initTestPrivateKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// test add file
|
||||
file1 := BlockRecordFile{Hash: testHashData([]byte("Test data")), Type: testTypeText, Format: testFormatText, Size: 9, ID: uuid.New()}
|
||||
file1.Tags = append(file1.Tags, TagFromText(TagName, "Test file to be deleted.txt"))
|
||||
file1.Tags = append(file1.Tags, TagFromText(TagFolder, "documents\\sub folder"))
|
||||
|
||||
newHeight, newVersion, status := blockchain.AddFiles([]BlockRecordFile{file1})
|
||||
fmt.Printf("Added file: Status %d height %d version %d\n", status, newHeight, newVersion)
|
||||
|
||||
// list files
|
||||
files, _ := blockchain.ListFiles()
|
||||
for _, file := range files {
|
||||
printFile(file)
|
||||
}
|
||||
|
||||
fmt.Printf("----------------\n")
|
||||
|
||||
// delete the file
|
||||
newHeight, newVersion, status = blockchain.DeleteFiles([]uuid.UUID{file1.ID})
|
||||
fmt.Printf("Deleted file: Status %d height %d version %d\n", status, newHeight, newVersion)
|
||||
|
||||
// list all files
|
||||
files, _ = blockchain.ListFiles()
|
||||
for _, file := range files {
|
||||
printFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockchainProfile(t *testing.T) {
|
||||
blockchain, err := initTestPrivateKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// write some test profile data
|
||||
newHeight, newVersion, status := blockchain.ProfileWrite([]BlockRecordProfile{
|
||||
ProfileFieldFromText(ProfileName, "Test User 1"),
|
||||
ProfileFieldFromText(ProfileEmail, "test@test.com"),
|
||||
{Type: 100, Data: []byte{0, 1, 2, 3}}})
|
||||
|
||||
fmt.Printf("Write profile data: Status %d height %d version %d\n", status, newHeight, newVersion)
|
||||
|
||||
// list all profile info
|
||||
printProfileData(blockchain)
|
||||
|
||||
fmt.Printf("----------------\n")
|
||||
|
||||
// delete profile info
|
||||
newHeight, newVersion, status = blockchain.ProfileDelete([]uint16{ProfileEmail})
|
||||
fmt.Printf("Deleted profile email: Status %d height %d version %d\n", status, newHeight, newVersion)
|
||||
|
||||
printProfileData(blockchain)
|
||||
}
|
||||
|
||||
func printProfileData(blockchain *Blockchain) {
|
||||
fields, status := blockchain.ProfileList()
|
||||
if status != BlockchainStatusOK {
|
||||
fmt.Printf("Reading profile data error status: %d\n", status)
|
||||
return
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
fmt.Printf("No profile data to visualize.\n")
|
||||
return
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
printProfileField(field)
|
||||
}
|
||||
}
|
||||
|
||||
func printProfileField(field BlockRecordProfile) {
|
||||
switch field.Type {
|
||||
case ProfileName, ProfileEmail, ProfileWebsite, ProfileTwitter, ProfileYouTube, ProfileAddress:
|
||||
fmt.Printf("* Field %d = %s\n", field.Type, string(field.Data))
|
||||
|
||||
default:
|
||||
fmt.Printf("* Field %d = %s\n", field.Type, hex.EncodeToString(field.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func testHashData(data []byte) (hash []byte) {
|
||||
hash32 := blake3.Sum256(data)
|
||||
return hash32[:]
|
||||
}
|
||||
|
||||
const testTypeText = 1
|
||||
const testFormatText = 10
|
||||
Reference in New Issue
Block a user