Persist transactions (#51)

* added account_state + changed ECPoint to PublicKey

* account state persist

* in depth test for existing accounts.

* implemented GetTransaction.

* added enrollment TX

* added persist of accounts and unspent coins

* bumped version -> 0.32.0
This commit is contained in:
Anthony De Meulemeester 2018-03-21 17:11:04 +01:00 committed by GitHub
parent a67728628e
commit 52fa41a12a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 930 additions and 66 deletions

View file

@ -1 +1 @@
0.31.0 0.32.0

137
pkg/core/account_state.go Normal file
View file

@ -0,0 +1,137 @@
package core
import (
"bytes"
"encoding/binary"
"io"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/util"
)
// Accounts is mapping between a account address and AccountState.
type Accounts map[util.Uint160]*AccountState
func (a Accounts) getAndChange(s storage.Store, hash util.Uint160) (*AccountState, error) {
if account, ok := a[hash]; ok {
return account, nil
}
var account *AccountState
key := storage.AppendPrefix(storage.STAccount, hash.Bytes())
if b, err := s.Get(key); err == nil {
if err := account.DecodeBinary(bytes.NewReader(b)); err != nil {
return nil, err
}
} else {
account = NewAccountState(hash)
}
a[hash] = account
return account, nil
}
// commit writes all account states to the given Batch.
func (a Accounts) commit(b storage.Batch) error {
buf := new(bytes.Buffer)
for hash, state := range a {
if err := state.EncodeBinary(buf); err != nil {
return err
}
key := storage.AppendPrefix(storage.STAccount, hash.Bytes())
b.Put(key, buf.Bytes())
buf.Reset()
}
return nil
}
// AccountState represents the state of a NEO account.
type AccountState struct {
ScriptHash util.Uint160
IsFrozen bool
Votes []*crypto.PublicKey
Balances map[util.Uint256]util.Fixed8
}
// NewAccountState returns a new AccountState object.
func NewAccountState(scriptHash util.Uint160) *AccountState {
return &AccountState{
ScriptHash: scriptHash,
IsFrozen: false,
Votes: []*crypto.PublicKey{},
Balances: make(map[util.Uint256]util.Fixed8),
}
}
// DecodeBinary decodes AccountState from the given io.Reader.
func (s *AccountState) DecodeBinary(r io.Reader) error {
if err := binary.Read(r, binary.LittleEndian, &s.ScriptHash); err != nil {
return err
}
if err := binary.Read(r, binary.LittleEndian, &s.IsFrozen); err != nil {
return err
}
lenVotes := util.ReadVarUint(r)
s.Votes = make([]*crypto.PublicKey, lenVotes)
for i := 0; i < int(lenVotes); i++ {
s.Votes[i] = &crypto.PublicKey{}
if err := s.Votes[i].DecodeBinary(r); err != nil {
return err
}
}
s.Balances = make(map[util.Uint256]util.Fixed8)
lenBalances := util.ReadVarUint(r)
for i := 0; i < int(lenBalances); i++ {
key := util.Uint256{}
if err := binary.Read(r, binary.LittleEndian, &key); err != nil {
return err
}
var val util.Fixed8
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return err
}
s.Balances[key] = val
}
return nil
}
// EncodeBinary encode AccountState to the given io.Writer.
func (s *AccountState) EncodeBinary(w io.Writer) error {
if err := binary.Write(w, binary.LittleEndian, s.ScriptHash); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, s.IsFrozen); err != nil {
return err
}
if err := util.WriteVarUint(w, uint64(len(s.Votes))); err != nil {
return err
}
for _, point := range s.Votes {
if err := point.EncodeBinary(w); err != nil {
return err
}
}
if err := util.WriteVarUint(w, uint64(len(s.Balances))); err != nil {
return err
}
for k, v := range s.Balances {
if v > 0 {
if err := binary.Write(w, binary.LittleEndian, k); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, v); err != nil {
return err
}
}
}
return nil
}

View file

@ -0,0 +1,47 @@
package core
import (
"bytes"
"testing"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestDecodeEncodeAccountState(t *testing.T) {
var (
n = 10
balances = make(map[util.Uint256]util.Fixed8)
votes = make([]*crypto.PublicKey, n)
)
for i := 0; i < n; i++ {
balances[util.RandomUint256()] = util.Fixed8(int64(util.RandomInt(1, 10000)))
votes[i] = &crypto.PublicKey{crypto.RandomECPoint()}
}
a := &AccountState{
ScriptHash: util.RandomUint160(),
IsFrozen: true,
Votes: votes,
Balances: balances,
}
buf := new(bytes.Buffer)
if err := a.EncodeBinary(buf); err != nil {
t.Fatal(err)
}
aDecode := &AccountState{}
if err := aDecode.DecodeBinary(buf); err != nil {
t.Fatal(err)
}
assert.Equal(t, a.ScriptHash, aDecode.ScriptHash)
assert.Equal(t, a.IsFrozen, aDecode.IsFrozen)
for i, vote := range a.Votes {
assert.Equal(t, vote.X, aDecode.Votes[i].X)
}
assert.Equal(t, a.Balances, aDecode.Balances)
}

View file

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -154,6 +155,13 @@ func (bc *Blockchain) run() {
} }
} }
// For now this will return a hardcoded hash of the NEO governing token.
func (bc *Blockchain) governingToken() util.Uint256 {
neoNativeAsset := "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b"
val, _ := util.Uint256DecodeString(neoNativeAsset)
return val
}
// AddBlock processes the given block and will add it to the cache so it // AddBlock processes the given block and will add it to the cache so it
// can be persisted. // can be persisted.
func (bc *Blockchain) AddBlock(block *Block) error { func (bc *Blockchain) AddBlock(block *Block) error {
@ -246,15 +254,99 @@ func (bc *Blockchain) processHeader(h *Header, batch storage.Batch, headerList *
return nil return nil
} }
// TODO: persistBlock needs some more love, its implemented as in the original
// project. This for the sake of development speed and understanding of what
// is happening here, quite allot as you can see :). If things are wired together
// and all tests are in place, we can make a more optimized and cleaner implementation.
func (bc *Blockchain) persistBlock(block *Block) error { func (bc *Blockchain) persistBlock(block *Block) error {
batch := bc.Batch() var (
batch = bc.Batch()
unspentCoins = make(UnspentCoins)
accounts = make(Accounts)
)
storeAsBlock(batch, block, 0) storeAsBlock(batch, block, 0)
storeAsCurrentBlock(batch, block) storeAsCurrentBlock(batch, block)
for _, tx := range block.Transactions {
storeAsTransaction(batch, tx, block.Index)
// Add CoinStateConfirmed for each tx output.
unspent := make([]CoinState, len(tx.Outputs))
for i := 0; i < len(tx.Outputs); i++ {
unspent[i] = CoinStateConfirmed
}
unspentCoins[tx.Hash()] = &UnspentCoinState{unspent}
// Process TX outputs.
for _, output := range tx.Outputs {
account, err := accounts.getAndChange(bc.Store, output.ScriptHash)
if err != nil {
return err
}
if _, ok := account.Balances[output.AssetID]; ok {
account.Balances[output.AssetID] += output.Amount
} else {
account.Balances[output.AssetID] = output.Amount
}
if output.AssetID.Equals(bc.governingToken()) && len(account.Votes) > 0 {
log.Warnf("governing token detected in TX output need to update validators!")
}
}
// Process TX inputs that are grouped by previous hash.
for prevHash, inputs := range tx.GroupInputsByPrevHash() {
prevTX, _, err := bc.GetTransaction(prevHash)
if err != nil {
return err
}
for _, input := range inputs {
unspent, err := unspentCoins.getAndChange(bc.Store, input.PrevHash)
if err != nil {
return err
}
unspent.states[input.PrevIndex] = CoinStateSpent
prevTXOutput := prevTX.Outputs[input.PrevIndex]
account, err := accounts.getAndChange(bc.Store, prevTXOutput.ScriptHash)
if err != nil {
return err
}
if prevTXOutput.AssetID.Equals(bc.governingToken()) {
log.Warnf("governing token detected in TX input need to update validators!")
}
account.Balances[prevTXOutput.AssetID] -= prevTXOutput.Amount
}
}
// Process the underlying type of the TX.
switch tx.Data.(type) {
case *transaction.RegisterTX:
case *transaction.IssueTX:
case *transaction.ClaimTX:
case *transaction.EnrollmentTX:
case *transaction.StateTX:
case *transaction.PublishTX:
case *transaction.InvocationTX:
log.Warn("invocation TX but we have no VM, o noo :(")
}
}
// Persist all to storage.
if err := accounts.commit(batch); err != nil {
return err
}
if err := unspentCoins.commit(batch); err != nil {
return err
}
if err := bc.PutBatch(batch); err != nil { if err := bc.PutBatch(batch); err != nil {
return err return err
} }
atomic.AddUint32(&bc.blockHeight, 1) atomic.AddUint32(&bc.blockHeight, 1)
return nil return nil
} }
@ -302,6 +394,27 @@ func (bc *Blockchain) headerListLen() (n int) {
return return
} }
// GetTransaction returns a TX and its height by the given hash.
func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) {
key := storage.AppendPrefix(storage.DataTransaction, hash.BytesReverse())
b, err := bc.Get(key)
if err != nil {
return nil, 0, err
}
r := bytes.NewReader(b)
var height uint32
if err := binary.Read(r, binary.LittleEndian, &height); err != nil {
return nil, 0, err
}
tx := &transaction.Transaction{}
if err := tx.DecodeBinary(r); err != nil {
return nil, 0, err
}
return tx, height, nil
}
// GetBlock returns a Block by the given hash. // GetBlock returns a Block by the given hash.
func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) { func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) {
key := storage.AppendPrefix(storage.DataBlock, hash.BytesReverse()) key := storage.AppendPrefix(storage.DataBlock, hash.BytesReverse())

View file

@ -122,6 +122,21 @@ func TestHasBlock(t *testing.T) {
assert.False(t, bc.HasBlock(newBlock.Hash())) assert.False(t, bc.HasBlock(newBlock.Hash()))
} }
func TestGetTransaction(t *testing.T) {
block := getDecodedBlock(t, 1)
bc := newTestChain(t)
assert.Nil(t, bc.AddBlock(block))
assert.Nil(t, bc.persistBlock(block))
tx, height, err := bc.GetTransaction(block.Transactions[0].Hash())
if err != nil {
t.Fatal(err)
}
assert.Equal(t, block.Index, height)
assert.Equal(t, block.Transactions[0], tx)
}
func newTestChain(t *testing.T) *Blockchain { func newTestChain(t *testing.T) *Blockchain {
startHash, _ := util.Uint256DecodeString("a") startHash, _ := util.Uint256DecodeString("a")
chain, err := NewBlockchain(storage.NewMemoryStore(), startHash) chain, err := NewBlockchain(storage.NewMemoryStore(), startHash)

12
pkg/core/coin_state.go Normal file
View file

@ -0,0 +1,12 @@
package core
// CoinState represents the state of a coin.
type CoinState uint8
// Viable CoinState constants.
const (
CoinStateConfirmed CoinState = 0
CoinStateSpent CoinState = 1 << 1
CoinStateClaimed CoinState = 1 << 2
CoinStateFrozen CoinState = 1 << 5
)

View file

@ -51,7 +51,8 @@ func (s *LevelDBStore) Seek(key []byte, f func(k, v []byte)) {
iter.Release() iter.Release()
} }
// Batch implements the Batch interface and returns a compatible Batch. // Batch implements the Batch interface and returns a leveldb
// compatible Batch.
func (s *LevelDBStore) Batch() Batch { func (s *LevelDBStore) Batch() Batch {
return new(leveldb.Batch) return new(leveldb.Batch)
} }

View file

@ -1,5 +1,9 @@
package storage package storage
import (
"encoding/hex"
)
// MemoryStore is an in-memory implementation of a Store, mainly // MemoryStore is an in-memory implementation of a Store, mainly
// used for testing. Do not use MemoryStore in production. // used for testing. Do not use MemoryStore in production.
type MemoryStore struct { type MemoryStore struct {
@ -31,7 +35,7 @@ func NewMemoryStore() *MemoryStore {
// Get implements the Store interface. // Get implements the Store interface.
func (s *MemoryStore) Get(key []byte) ([]byte, error) { func (s *MemoryStore) Get(key []byte) ([]byte, error) {
if val, ok := s.mem[string(key)]; ok { if val, ok := s.mem[makeKey(key)]; ok {
return val, nil return val, nil
} }
return nil, ErrKeyNotFound return nil, ErrKeyNotFound
@ -39,7 +43,7 @@ func (s *MemoryStore) Get(key []byte) ([]byte, error) {
// Put implementes the Store interface. // Put implementes the Store interface.
func (s *MemoryStore) Put(key, value []byte) error { func (s *MemoryStore) Put(key, value []byte) error {
s.mem[string(key)] = value s.mem[makeKey(key)] = value
return nil return nil
} }
@ -62,3 +66,7 @@ func (s *MemoryStore) Batch() Batch {
m: make(map[*[]byte][]byte), m: make(map[*[]byte][]byte),
} }
} }
func makeKey(k []byte) string {
return hex.EncodeToString(k)
}

View file

@ -22,6 +22,17 @@ func TestGetPut(t *testing.T) {
assert.Equal(t, value, newVal) assert.Equal(t, value, newVal)
} }
func TestKeyNotExist(t *testing.T) {
var (
s = NewMemoryStore()
key = []byte("sparse")
)
_, err := s.Get(key)
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "key not found")
}
func TestPutBatch(t *testing.T) { func TestPutBatch(t *testing.T) {
var ( var (
s = NewMemoryStore() s = NewMemoryStore()

View file

@ -11,6 +11,7 @@ const (
DataTransaction KeyPrefix = 0x02 DataTransaction KeyPrefix = 0x02
STAccount KeyPrefix = 0x40 STAccount KeyPrefix = 0x40
STCoin KeyPrefix = 0x44 STCoin KeyPrefix = 0x44
STSpentCoin KeyPrefix = 0x45
STValidator KeyPrefix = 0x48 STValidator KeyPrefix = 0x48
STAsset KeyPrefix = 0x4c STAsset KeyPrefix = 0x4c
STContract KeyPrefix = 0x50 STContract KeyPrefix = 0x50

View file

@ -0,0 +1,28 @@
package transaction
import (
"io"
"github.com/CityOfZion/neo-go/pkg/crypto"
)
// A Enrollment transaction represents an enrollment form, which indicates
// that the sponsor of the transaction would like to sign up as a validator.
// The way to sign up is: To construct an EnrollmentTransaction type of transaction,
// and send a deposit to the address of the PublicKey.
// The way to cancel the registration is: Spend the deposit on the address of the PublicKey.
type EnrollmentTX struct {
// PublicKey of the validator
PublicKey *crypto.PublicKey
}
// DecodeBinary implements the Payload interface.
func (tx *EnrollmentTX) DecodeBinary(r io.Reader) error {
tx.PublicKey = &crypto.PublicKey{}
return tx.PublicKey.DecodeBinary(r)
}
// EncodeBinary implements the Payload interface.
func (tx *EnrollmentTX) EncodeBinary(w io.Writer) error {
return tx.PublicKey.EncodeBinary(w)
}

View file

@ -0,0 +1,6 @@
package transaction
import "testing"
func TestDecodeEncode(t *testing.T) {
}

View file

@ -4,7 +4,7 @@ import (
"encoding/binary" "encoding/binary"
"io" "io"
"github.com/anthdm/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
) )
// Input represents a Transaction input. // Input represents a Transaction input.

View file

@ -0,0 +1,30 @@
package transaction
import (
"io"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
)
// PublishTX represents a publish transaction.
// This is deprecated and should no longer be used.
type PublishTX struct {
Script []byte
ParamList []smartcontract.ParamType
ReturnType smartcontract.ParamType
NeedStorage bool
Name string
Author string
Email string
Description string
}
// DecodeBinary implements the Payload interface.
func (tx *PublishTX) DecodeBinary(r io.Reader) error {
return nil
}
// EncodeBinary implements the Payload interface.
func (tx *PublishTX) EncodeBinary(w io.Writer) error {
return nil
}

View file

@ -29,7 +29,7 @@ import (
// # 2. For currencies, use only unlimited models; // # 2. For currencies, use only unlimited models;
// # 3. For point coupons, you can use any pattern; // # 3. For point coupons, you can use any pattern;
// RegisterTx represents a register transaction. // RegisterTX represents a register transaction.
type RegisterTX struct { type RegisterTX struct {
// The type of the asset being registered. // The type of the asset being registered.
AssetType AssetType AssetType AssetType
@ -44,7 +44,9 @@ type RegisterTX struct {
// Decimals // Decimals
Precision uint8 Precision uint8
Owner crypto.EllipticCurvePoint // Public key of the owner
Owner *crypto.PublicKey
Admin util.Uint160 Admin util.Uint160
} }
@ -65,11 +67,10 @@ func (tx *RegisterTX) DecodeBinary(r io.Reader) error {
return err return err
} }
point, err := crypto.NewEllipticCurvePointFromReader(r) tx.Owner = &crypto.PublicKey{}
if err != nil { if err := tx.Owner.DecodeBinary(r); err != nil {
return err return err
} }
tx.Owner = point
return binary.Read(r, binary.LittleEndian, &tx.Admin) return binary.Read(r, binary.LittleEndian, &tx.Admin)
} }

View file

@ -0,0 +1,29 @@
package transaction
import (
"io"
"github.com/CityOfZion/neo-go/pkg/util"
)
// StateTX represents a state transaction.
type StateTX struct {
Descriptors []*StateDescriptor
}
// DecodeBinary implements the Payload interface.
func (tx *StateTX) DecodeBinary(r io.Reader) error {
lenDesc := util.ReadVarUint(r)
for i := 0; i < int(lenDesc); i++ {
tx.Descriptors[i] = &StateDescriptor{}
if err := tx.Descriptors[i].DecodeBinary(r); err != nil {
return err
}
}
return nil
}
// EncodeBinary implements the Payload interface.
func (tx *StateTX) EncodeBinary(w io.Writer) error {
return nil
}

View file

@ -0,0 +1,58 @@
package transaction
import (
"encoding/binary"
"io"
"github.com/CityOfZion/neo-go/pkg/util"
)
// DescStateType represents the type of StateDescriptor.
type DescStateType uint8
// Valid DescStateType constants.
const (
Account DescStateType = 0x40
Validator DescStateType = 0x48
)
// StateDescriptor ..
type StateDescriptor struct {
Type DescStateType
Key []byte
Value []byte
Field string
}
// DecodeBinary implements the Payload interface.
func (s *StateDescriptor) DecodeBinary(r io.Reader) error {
if err := binary.Read(r, binary.LittleEndian, &s.Type); err != nil {
return err
}
keyLen := util.ReadVarUint(r)
s.Key = make([]byte, keyLen)
if err := binary.Read(r, binary.LittleEndian, s.Key); err != nil {
return err
}
valLen := util.ReadVarUint(r)
s.Value = make([]byte, valLen)
if err := binary.Read(r, binary.LittleEndian, s.Value); err != nil {
return err
}
fieldLen := util.ReadVarUint(r)
field := make([]byte, fieldLen)
if err := binary.Read(r, binary.LittleEndian, field); err != nil {
return err
}
s.Field = string(field)
return nil
}
// EncodeBinary implements the Payload interface.
func (s *StateDescriptor) EncodeBinary(w io.Writer) error {
return nil
}

View file

@ -147,6 +147,10 @@ func (t *Transaction) decodeData(r io.Reader) error {
case IssueType: case IssueType:
t.Data = &IssueTX{} t.Data = &IssueTX{}
return t.Data.(*IssueTX).DecodeBinary(r) return t.Data.(*IssueTX).DecodeBinary(r)
case EnrollmentType:
t.Data = &EnrollmentTX{}
return t.Data.(*EnrollmentTX).DecodeBinary(r)
default: default:
log.Warnf("invalid TX type %s", t.Type) log.Warnf("invalid TX type %s", t.Type)
} }
@ -180,8 +184,10 @@ func (t *Transaction) encodeHashableFields(w io.Writer) error {
} }
// Underlying TXer. // Underlying TXer.
if err := t.Data.EncodeBinary(w); err != nil { if t.Data != nil {
return err if err := t.Data.EncodeBinary(w); err != nil {
return err
}
} }
// Attributes // Attributes
@ -230,3 +236,12 @@ func (t *Transaction) createHash() (hash util.Uint256, err error) {
b = sha.Sum(nil) b = sha.Sum(nil)
return util.Uint256DecodeBytes(util.ArrayReverse(b)) return util.Uint256DecodeBytes(util.ArrayReverse(b))
} }
// GroupTXInputsByPrevHash groups all TX inputs by their previous hash.
func (t *Transaction) GroupInputsByPrevHash() map[util.Uint256][]*Input {
m := make(map[util.Uint256][]*Input)
for _, in := range t.Inputs {
m[in.PrevHash] = append(m[in.PrevHash], in)
}
return m
}

View file

@ -0,0 +1,81 @@
package core
import (
"bytes"
"encoding/binary"
"io"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/util"
)
// UnspentCoins is mapping between transactions and their unspent
// coin state.
type UnspentCoins map[util.Uint256]*UnspentCoinState
func (u UnspentCoins) getAndChange(s storage.Store, hash util.Uint256) (*UnspentCoinState, error) {
if unspent, ok := u[hash]; ok {
return unspent, nil
}
var unspent *UnspentCoinState
key := storage.AppendPrefix(storage.STCoin, hash.BytesReverse())
if b, err := s.Get(key); err == nil {
if err := unspent.DecodeBinary(bytes.NewReader(b)); err != nil {
return nil, err
}
} else {
unspent = &UnspentCoinState{
states: []CoinState{},
}
}
u[hash] = unspent
return unspent, nil
}
// UnspentCoinState hold the state of a unspent coin.
type UnspentCoinState struct {
states []CoinState
}
// commit writes all unspent coin states to the given Batch.
func (s UnspentCoins) commit(b storage.Batch) error {
buf := new(bytes.Buffer)
for hash, state := range s {
if err := state.EncodeBinary(buf); err != nil {
return err
}
key := storage.AppendPrefix(storage.STCoin, hash.BytesReverse())
b.Put(key, buf.Bytes())
buf.Reset()
}
return nil
}
// EncodeBinary encodes UnspentCoinState to the given io.Writer.
func (s *UnspentCoinState) EncodeBinary(w io.Writer) error {
if err := util.WriteVarUint(w, uint64(len(s.states))); err != nil {
return err
}
for _, state := range s.states {
if err := binary.Write(w, binary.LittleEndian, byte(state)); err != nil {
return err
}
}
return nil
}
// DecodBinary decodes UnspentCoinState from the given io.Reader.
func (s *UnspentCoinState) DecodeBinary(r io.Reader) error {
lenStates := util.ReadVarUint(r)
s.states = make([]CoinState, lenStates)
for i := 0; i < int(lenStates); i++ {
var state uint8
if err := binary.Read(r, binary.LittleEndian, &state); err != nil {
return err
}
s.states[i] = CoinState(state)
}
return nil
}

View file

@ -0,0 +1,59 @@
package core
import (
"bytes"
"testing"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestDecodeEncode(t *testing.T) {
unspent := &UnspentCoinState{
states: []CoinState{
CoinStateConfirmed,
CoinStateSpent,
CoinStateSpent,
CoinStateSpent,
CoinStateConfirmed,
},
}
buf := new(bytes.Buffer)
assert.Nil(t, unspent.EncodeBinary(buf))
unspentDecode := &UnspentCoinState{}
assert.Nil(t, unspentDecode.DecodeBinary(buf))
}
func TestCommit(t *testing.T) {
var (
store = storage.NewMemoryStore()
batch = store.Batch()
unspentCoins = make(UnspentCoins)
)
txA := util.RandomUint256()
txB := util.RandomUint256()
txC := util.RandomUint256()
unspentCoins[txA] = &UnspentCoinState{
states: []CoinState{CoinStateConfirmed},
}
unspentCoins[txB] = &UnspentCoinState{
states: []CoinState{
CoinStateConfirmed,
CoinStateConfirmed,
},
}
unspentCoins[txC] = &UnspentCoinState{
states: []CoinState{
CoinStateConfirmed,
CoinStateConfirmed,
CoinStateConfirmed,
},
}
assert.Nil(t, unspentCoins.commit(batch))
assert.Nil(t, store.PutBatch(batch))
}

View file

@ -6,6 +6,7 @@ import (
"sort" "sort"
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
) )
@ -65,6 +66,22 @@ func storeAsBlock(batch storage.Batch, block *Block, sysFee uint32) error {
return nil return nil
} }
// storeAsTransaction stores the given TX as DataTransaction.
func storeAsTransaction(batch storage.Batch, tx *transaction.Transaction, index uint32) error {
key := storage.AppendPrefix(storage.DataTransaction, tx.Hash().BytesReverse())
buf := new(bytes.Buffer)
if err := tx.EncodeBinary(buf); err != nil {
return err
}
dest := make([]byte, buf.Len()+4)
binary.LittleEndian.PutUint32(dest[:4], index)
copy(dest[4:], buf.Bytes())
batch.Put(key, dest)
return nil
}
// readStoredHeaderHashes returns a sorted list of header hashes // readStoredHeaderHashes returns a sorted list of header hashes
// retrieved from the given Store. // retrieved from the given Store.
func readStoredHeaderHashes(store storage.Store) ([]util.Uint256, error) { func readStoredHeaderHashes(store storage.Store) ([]util.Uint256, error) {

View file

@ -0,0 +1,16 @@
package core
import (
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/util"
)
// Validators is a mapping between public keys and ValidatorState.
type Validators map[*crypto.PublicKey]*ValidatorState
// ValidatorState holds the state of a validator.
type ValidatorState struct {
PublicKey *crypto.PublicKey
Registered bool
Votes util.Fixed8
}

View file

@ -4,6 +4,8 @@ package crypto
// Expanded and tweaked upon here under MIT license. // Expanded and tweaked upon here under MIT license.
import ( import (
"bytes"
"crypto/rand"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
@ -21,51 +23,18 @@ type (
A *big.Int A *big.Int
B *big.Int B *big.Int
P *big.Int P *big.Int
G EllipticCurvePoint G ECPoint
N *big.Int N *big.Int
H *big.Int H *big.Int
} }
// EllipticCurveEllipticCurvePoint represents a point on the EllipticCurve. // ECPoint represents a point on the EllipticCurve.
EllipticCurvePoint struct { ECPoint struct {
X *big.Int X *big.Int
Y *big.Int Y *big.Int
} }
) )
// NewEllipticCurvePointFromReader return a new point from the given reader.
// f == 4, 6 or 7 are not implemented.
func NewEllipticCurvePointFromReader(r io.Reader) (point EllipticCurvePoint, err error) {
var f uint8
if err = binary.Read(r, binary.LittleEndian, &f); err != nil {
return
}
// Infinity
if f == 0 {
return EllipticCurvePoint{
X: new(big.Int),
Y: new(big.Int),
}, nil
}
if f == 2 || f == 3 {
y := new(big.Int).SetBytes([]byte{f & 1})
data := make([]byte, 32)
if err = binary.Read(r, binary.LittleEndian, data); err != nil {
return
}
data = util.ArrayReverse(data)
data = append(data, byte(0x00))
return EllipticCurvePoint{
X: new(big.Int).SetBytes(data),
Y: y,
}, nil
}
return
}
// NewEllipticCurve returns a ready to use EllipticCurve with preconfigured // NewEllipticCurve returns a ready to use EllipticCurve with preconfigured
// fields for the NEO protocol. // fields for the NEO protocol.
func NewEllipticCurve() EllipticCurve { func NewEllipticCurve() EllipticCurve {
@ -94,9 +63,84 @@ func NewEllipticCurve() EllipticCurve {
return c return c
} }
func (p *EllipticCurvePoint) format() string { // RandomECPoint returns a random generated ECPoint, mostly used
if p.X == nil && p.Y == nil { // for testing.
return "(inf,inf)" func RandomECPoint() ECPoint {
c := NewEllipticCurve()
b := make([]byte, c.N.BitLen()/8+8)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ECPoint{}
}
d := new(big.Int).SetBytes(b)
d.Mod(d, new(big.Int).Sub(c.N, big.NewInt(1)))
d.Add(d, big.NewInt(1))
q := new(big.Int).SetBytes(d.Bytes())
return c.ScalarBaseMult(q)
}
// ECPointFromReader return a new point from the given reader.
// f == 4, 6 or 7 are not implemented.
func ECPointFromReader(r io.Reader) (point ECPoint, err error) {
var f uint8
if err = binary.Read(r, binary.LittleEndian, &f); err != nil {
return
}
fmt.Println(f)
// Infinity
if f == 0 {
return ECPoint{
X: new(big.Int),
Y: new(big.Int),
}, nil
}
if f == 2 || f == 3 {
y := new(big.Int).SetBytes([]byte{f & 1})
data := make([]byte, 32)
if err = binary.Read(r, binary.LittleEndian, data); err != nil {
return
}
data = util.ArrayReverse(data)
data = append(data, byte(0x00))
return ECPoint{
X: new(big.Int).SetBytes(data),
Y: y,
}, nil
}
return
}
// EncodeBinary encodes the point to the given io.Writer.
func (p ECPoint) EncodeBinary(w io.Writer) error {
bx := p.X.Bytes()
padded := append(
bytes.Repeat(
[]byte{0x00},
32-len(bx),
),
bx...,
)
prefix := byte(0x03)
if p.Y.Bit(0) == 0 {
prefix = byte(0x02)
}
buf := make([]byte, len(padded)+1)
buf[0] = prefix
copy(buf[1:], padded)
return binary.Write(w, binary.LittleEndian, buf)
}
// String implements the Stringer interface.
func (p *ECPoint) String() string {
if p.IsInfinity() {
return "(inf, inf)"
} }
bx := hex.EncodeToString(p.X.Bytes()) bx := hex.EncodeToString(p.X.Bytes())
by := hex.EncodeToString(p.Y.Bytes()) by := hex.EncodeToString(p.Y.Bytes())
@ -104,15 +148,17 @@ func (p *EllipticCurvePoint) format() string {
} }
// IsInfinity checks if point P is infinity on EllipticCurve ec. // IsInfinity checks if point P is infinity on EllipticCurve ec.
func (c *EllipticCurve) IsInfinity(P EllipticCurvePoint) bool { func (p *ECPoint) IsInfinity() bool {
if P.X == nil && P.Y == nil { return p.X == nil && p.Y == nil
return true }
}
return false // IsInfinity checks if point P is infinity on EllipticCurve ec.
func (c *EllipticCurve) IsInfinity(P ECPoint) bool {
return P.X == nil && P.Y == nil
} }
// IsOnCurve checks if point P is on EllipticCurve ec. // IsOnCurve checks if point P is on EllipticCurve ec.
func (c *EllipticCurve) IsOnCurve(P EllipticCurvePoint) bool { func (c *EllipticCurve) IsOnCurve(P ECPoint) bool {
if c.IsInfinity(P) { if c.IsInfinity(P) {
return false return false
} }
@ -130,7 +176,7 @@ func (c *EllipticCurve) IsOnCurve(P EllipticCurvePoint) bool {
} }
// Add computes R = P + Q on EllipticCurve ec. // Add computes R = P + Q on EllipticCurve ec.
func (c *EllipticCurve) Add(P, Q EllipticCurvePoint) (R EllipticCurvePoint) { func (c *EllipticCurve) Add(P, Q ECPoint) (R ECPoint) {
// See rules 1-5 on SEC1 pg.7 http://www.secg.org/collateral/sec1_final.pdf // See rules 1-5 on SEC1 pg.7 http://www.secg.org/collateral/sec1_final.pdf
if c.IsInfinity(P) && c.IsInfinity(Q) { if c.IsInfinity(P) && c.IsInfinity(Q) {
R.X = nil R.X = nil
@ -172,18 +218,18 @@ func (c *EllipticCurve) Add(P, Q EllipticCurvePoint) (R EllipticCurvePoint) {
subMod(P.X, R.X, c.P), c.P), subMod(P.X, R.X, c.P), c.P),
P.Y, c.P) P.Y, c.P)
} else { } else {
panic(fmt.Sprintf("Unsupported point addition: %v + %v", P.format(), Q.format())) panic(fmt.Sprintf("Unsupported point addition: %v + %v", P, Q))
} }
return R return R
} }
// ScalarMult computes Q = k * P on EllipticCurve ec. // ScalarMult computes Q = k * P on EllipticCurve ec.
func (c *EllipticCurve) ScalarMult(k *big.Int, P EllipticCurvePoint) (Q EllipticCurvePoint) { func (c *EllipticCurve) ScalarMult(k *big.Int, P ECPoint) (Q ECPoint) {
// Implementation based on pseudocode here: // Implementation based on pseudocode here:
// https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder // https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder
var R0 EllipticCurvePoint var R0 ECPoint
var R1 EllipticCurvePoint var R1 ECPoint
R0.X = nil R0.X = nil
R0.Y = nil R0.Y = nil
@ -203,12 +249,12 @@ func (c *EllipticCurve) ScalarMult(k *big.Int, P EllipticCurvePoint) (Q Elliptic
} }
// ScalarBaseMult computes Q = k * G on EllipticCurve ec. // ScalarBaseMult computes Q = k * G on EllipticCurve ec.
func (c *EllipticCurve) ScalarBaseMult(k *big.Int) (Q EllipticCurvePoint) { func (c *EllipticCurve) ScalarBaseMult(k *big.Int) (Q ECPoint) {
return c.ScalarMult(k, c.G) return c.ScalarMult(k, c.G)
} }
// Decompress decompresses coordinate x and ylsb (y's least significant bit) into a EllipticCurvePoint P on EllipticCurve ec. // Decompress decompresses coordinate x and ylsb (y's least significant bit) into a ECPoint P on EllipticCurve ec.
func (c *EllipticCurve) Decompress(x *big.Int, ylsb uint) (P EllipticCurvePoint, err error) { func (c *EllipticCurve) Decompress(x *big.Int, ylsb uint) (P ECPoint, err error) {
/* y**2 = x**3 + a*x + b % p */ /* y**2 = x**3 + a*x + b % p */
rhs := addMod( rhs := addMod(
addMod( addMod(

68
pkg/crypto/public_key.go Normal file
View file

@ -0,0 +1,68 @@
package crypto
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math/big"
)
// PublicKey represents a public key.
type PublicKey struct {
ECPoint
}
// Bytes returns the byte array representation of the public key.
func (p *PublicKey) Bytes() []byte {
var (
x = p.X.Bytes()
paddedX = append(bytes.Repeat([]byte{0x00}, 32-len(x)), x...)
prefix = byte(0x03)
)
if p.Y.Bit(0) == 0 {
prefix = byte(0x02)
}
return append([]byte{prefix}, paddedX...)
}
// DecodeBinary decodes a PublicKey from the given io.Reader.
func (p *PublicKey) DecodeBinary(r io.Reader) error {
var prefix uint8
if err := binary.Read(r, binary.LittleEndian, &prefix); err != nil {
return err
}
// Compressed public keys.
if prefix == 0x02 || prefix == 0x03 {
c := NewEllipticCurve()
b := make([]byte, 32)
if err := binary.Read(r, binary.LittleEndian, b); err != nil {
return err
}
var err error
p.ECPoint, err = c.Decompress(new(big.Int).SetBytes(b), uint(prefix&0x1))
if err != nil {
return err
}
} else if prefix == 0x04 {
buf := make([]byte, 65)
if err := binary.Read(r, binary.LittleEndian, buf); err != nil {
return err
}
p.X = new(big.Int).SetBytes(buf[1:33])
p.Y = new(big.Int).SetBytes(buf[33:65])
} else {
return fmt.Errorf("invalid prefix %d", prefix)
}
return nil
}
// EncodeBinary encodes a PublicKey to the given io.Writer.
func (p *PublicKey) EncodeBinary(w io.Writer) error {
return binary.Write(w, binary.LittleEndian, p.Bytes())
}

View file

@ -0,0 +1,20 @@
package crypto
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecodePublicKey(t *testing.T) {
for i := 0; i < 4; i++ {
p := &PublicKey{RandomECPoint()}
buf := new(bytes.Buffer)
assert.Nil(t, p.EncodeBinary(buf))
pDecode := &PublicKey{}
assert.Nil(t, pDecode.DecodeBinary(buf))
assert.Equal(t, p.X, pDecode.X)
}
}

45
pkg/util/test_util.go Normal file
View file

@ -0,0 +1,45 @@
package util
import (
"crypto/sha256"
"math/rand"
"time"
"golang.org/x/crypto/ripemd160"
)
// RandomString returns a random string with the n as its length.
func RandomString(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = byte(RandomInt(65, 90))
}
return string(b)
}
// RamdomInt returns a ramdom integer betweeen min and max.
func RandomInt(min, max int) int {
return min + rand.Intn(max-min)
}
// RandomUint256 returns a random Uint256.
func RandomUint256() Uint256 {
str := RandomString(20)
h := sha256.Sum256([]byte(str))
return Uint256(h)
}
// RandomUint160 returns a random Uint160.
func RandomUint160() Uint160 {
str := RandomString(20)
ripemd := ripemd160.New()
ripemd.Write([]byte(str))
h := ripemd.Sum(nil)
v, _ := Uint160DecodeBytes(h)
return v
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}