Files
core/Blockchain.go
Kleissner a2384ca627 Blockchain: Change of block format.
File records:
* Add unique ID to file record.
* Introducing tags which are flexible. Close #31
* Name and Directory fields are now tags.
* Embedded basic compression by storing duplicate file tags as single record.
Added date field to record structure.
2021-08-27 03:12:30 +02:00

251 lines
8.7 KiB
Go

/*
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 core
import (
"encoding/binary"
"encoding/hex"
"errors"
"os"
"sync"
"github.com/PeernetOfficial/core/store"
"github.com/btcsuite/btcd/btcec"
)
// BlockchainHeight is the current count of blocks
var BlockchainHeight = uint32(0)
// BlockchainVersion is the version of the blockchain
var BlockchainVersion = uint64(0)
// filenameUserBlockchain is the filename/folder of the user's blockchain
const filenameUserBlockchain = "self.blockchain"
// 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"
// userBlockchainHeader stores the users blockchain header in memory. Any changes must be synced to disk!
var userBlockchainHeader struct {
height uint64
version uint64
format uint16
publicKey *btcec.PublicKey
sync.Mutex
}
var userBlockchainDB store.Store
// initUserBlockchain initializes the users blockchain. It creates the blockchain file if it does not exist already.
// If it is corrupted, it will log the error and exit the process.
func initUserBlockchain() {
// open existing blockchain file or create new one
var err error
if userBlockchainDB, err = store.NewPogrebStore(filenameUserBlockchain); err != nil {
Filters.LogError("initUserBlockchain", "error opening user blockchain: %s\n", err.Error())
os.Exit(1)
}
// verify header
var found bool
userBlockchainHeader.publicKey, userBlockchainHeader.height, userBlockchainHeader.version, found, err = blockchainHeaderRead(userBlockchainDB)
if err != nil {
Filters.LogError("initUserBlockchain", "corrupt user blockchain database: %s\n", err.Error())
os.Exit(1)
} else if !found {
// First run: create header signature!
userBlockchainHeader.height = 0
userBlockchainHeader.version = 0
if err := blockchainHeaderWrite(userBlockchainDB, peerPrivateKey, userBlockchainHeader.height, userBlockchainHeader.version); err != nil {
Filters.LogError("initUserBlockchain", "initializing user blockchain: %s", err.Error())
os.Exit(1)
}
} else if !userBlockchainHeader.publicKey.IsEqual(peerPublicKey) {
Filters.LogError("initUserBlockchain", "corrupt user blockchain database. Public key mismatch. Height is '%d', version '%d'. Public key expected '%s' vs provided '%s'\n", userBlockchainHeader.height, userBlockchainHeader.version, hex.EncodeToString(peerPublicKey.SerializeCompressed()), hex.EncodeToString(userBlockchainHeader.publicKey.SerializeCompressed()))
os.Exit(1)
}
}
// blockchainHeaderRead reads the header from the blockchain and decodes it.
func blockchainHeaderRead(db store.Store) (publicKey *btcec.PublicKey, height, version uint64, found bool, err error) {
buffer, found := db.Get([]byte(keyHeader))
if !found {
return nil, 0, 0, false, nil
}
if len(buffer) != 83 {
return nil, 0, 0, true, errors.New("blockchain header size mismatch")
}
height = binary.LittleEndian.Uint64(buffer[0:8])
version = binary.LittleEndian.Uint64(buffer[8:16])
format := binary.LittleEndian.Uint16(buffer[16:18])
signature := buffer[18 : 18+65]
if format != 0 {
return nil, 0, 0, true, errors.New("future blockchain format not supported. You must go back to the future!")
}
publicKey, _, err = btcec.RecoverCompact(btcec.S256(), signature, hashData(buffer[0:18]))
return
}
// blockchainHeaderWrite writes the header to the blockchain and signs it.
func blockchainHeaderWrite(db store.Store, privateKey *btcec.PrivateKey, height, version uint64) (err error) {
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(), privateKey, hashData(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 = db.Set([]byte(keyHeader), buffer[:])
return err
}
// UserBlockchainHeader returns the users blockchain header which stores the height and version number.
func UserBlockchainHeader() (publicKey *btcec.PublicKey, height uint64, version uint64) {
return userBlockchainHeader.publicKey, userBlockchainHeader.height, userBlockchainHeader.version
}
// ---- low-level blockchain manipulation functions ----
// UserBlockchainAppend appends a new block to the blockchain based on the provided raw records.
// Status: 0 = Success, 1 = Error previous block not found, 2 = Error block encoding
func UserBlockchainAppend(RecordsRaw []BlockRecordRaw) (newHeight uint64, status int) {
userBlockchainHeader.Lock()
defer userBlockchainHeader.Unlock()
block := &Block{OwnerPublicKey: peerPublicKey, RecordsRaw: RecordsRaw}
// set the last block hash first
if userBlockchainHeader.height > 0 {
var target [8]byte
binary.LittleEndian.PutUint64(target[:], userBlockchainHeader.height-1)
previousBlockRaw, found := userBlockchainDB.Get(target[:])
if !found || len(previousBlockRaw) == 0 {
return 0, 1
}
block.LastBlockHash = hashData(previousBlockRaw)
}
block.Number = userBlockchainHeader.height
block.BlockchainVersion = userBlockchainHeader.version
raw, err := encodeBlock(block, peerPrivateKey)
if err != nil {
return 0, 2
}
// increase blockchain height
userBlockchainHeader.height++
// store the block
var numberB [8]byte
binary.LittleEndian.PutUint64(numberB[:], block.Number)
userBlockchainDB.Set(numberB[:], raw)
// update the blockchain header in the database
blockchainHeaderWrite(userBlockchainDB, peerPrivateKey, userBlockchainHeader.height, userBlockchainHeader.version)
return userBlockchainHeader.height, 0
}
// UserBlockchainRead reads the block number from the blockchain.
// Status: 0 = Success, 1 = Error block not found, 2 = Error block encoding, 3 = Error block record encoding
// Errors 2 and 3 indicate data corruption.
func UserBlockchainRead(number uint64) (decoded *BlockDecoded, status int, err error) {
if number >= userBlockchainHeader.height {
return nil, 1, errors.New("block number exceeds blockchain height")
}
var target [8]byte
binary.LittleEndian.PutUint64(target[:], userBlockchainHeader.height-1)
blockRaw, found := userBlockchainDB.Get(target[:])
if !found || len(blockRaw) == 0 {
return nil, 1, errors.New("block not found")
}
block, err := decodeBlock(blockRaw)
if err != nil {
return nil, 2, err
}
decoded, err = decodeBlockRecords(block)
if err != nil {
return nil, 2, err
}
return decoded, 0, nil
}
// UserBlockchainAddFiles adds files to the blockchain
// Status: 0 = Success, 1 = Error previous block not found, 2 = Error block encoding, 3 = Error block record encoding
// 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 UserBlockchainAddFiles(files []BlockRecordFile) (newHeight uint64, status int) {
encoded, err := encodeBlockRecordFiles(files)
if err != nil {
return 0, 3
}
return UserBlockchainAppend(encoded)
}
// UserBlockchainListFiles returns a list of all files
// If there is a corruption in the blockchain it will reading it but return the files parsed so far.
// Status: 0 = Success, 1 = Block not found, 2 = Error block encoding, 3 = Error block record encoding
func UserBlockchainListFiles() (files []BlockRecordFile, status int) {
// TODO: Add internal cache of file list for faster subsequent processing?
height := userBlockchainHeader.height
// read all blocks until height is reached
for blockN := uint64(0); blockN < height; blockN++ {
var target [8]byte
binary.LittleEndian.PutUint64(target[:], userBlockchainHeader.height-1)
blockRaw, found := userBlockchainDB.Get(target[:])
if !found || len(blockRaw) == 0 {
return files, 1
}
block, err := decodeBlock(blockRaw)
if err != nil {
return files, 2
}
filesMore, err := decodeBlockRecordFiles(block.RecordsRaw)
if err != nil {
return nil, 3
}
files = append(files, filesMore...)
}
return files, 0
}