diff --git a/VERSION b/VERSION index 26bea73e8..9eb2aa3f1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.31.0 +0.32.0 diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go new file mode 100644 index 000000000..f32bb0bd6 --- /dev/null +++ b/pkg/core/account_state.go @@ -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 +} diff --git a/pkg/core/account_state_test.go b/pkg/core/account_state_test.go new file mode 100644 index 000000000..043891da8 --- /dev/null +++ b/pkg/core/account_state_test.go @@ -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) +} diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 63580868d..b64de75a8 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -9,6 +9,7 @@ import ( "time" "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" 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 // can be persisted. func (bc *Blockchain) AddBlock(block *Block) error { @@ -246,15 +254,99 @@ func (bc *Blockchain) processHeader(h *Header, batch storage.Batch, headerList * 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 { - batch := bc.Batch() + var ( + batch = bc.Batch() + unspentCoins = make(UnspentCoins) + accounts = make(Accounts) + ) storeAsBlock(batch, block, 0) 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 { return err } + atomic.AddUint32(&bc.blockHeight, 1) return nil } @@ -302,6 +394,27 @@ func (bc *Blockchain) headerListLen() (n int) { 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. func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) { key := storage.AppendPrefix(storage.DataBlock, hash.BytesReverse()) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 8d4e0c5e0..109a557a7 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -122,6 +122,21 @@ func TestHasBlock(t *testing.T) { 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 { startHash, _ := util.Uint256DecodeString("a") chain, err := NewBlockchain(storage.NewMemoryStore(), startHash) diff --git a/pkg/core/coin_state.go b/pkg/core/coin_state.go new file mode 100644 index 000000000..8232977f3 --- /dev/null +++ b/pkg/core/coin_state.go @@ -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 +) diff --git a/pkg/core/storage/leveldb_store.go b/pkg/core/storage/leveldb_store.go index 71e83f11f..a847a3db3 100644 --- a/pkg/core/storage/leveldb_store.go +++ b/pkg/core/storage/leveldb_store.go @@ -51,7 +51,8 @@ func (s *LevelDBStore) Seek(key []byte, f func(k, v []byte)) { 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 { return new(leveldb.Batch) } diff --git a/pkg/core/storage/memory_store.go b/pkg/core/storage/memory_store.go index a63813a92..e18147490 100644 --- a/pkg/core/storage/memory_store.go +++ b/pkg/core/storage/memory_store.go @@ -1,5 +1,9 @@ package storage +import ( + "encoding/hex" +) + // MemoryStore is an in-memory implementation of a Store, mainly // used for testing. Do not use MemoryStore in production. type MemoryStore struct { @@ -31,7 +35,7 @@ func NewMemoryStore() *MemoryStore { // Get implements the Store interface. 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 nil, ErrKeyNotFound @@ -39,7 +43,7 @@ func (s *MemoryStore) Get(key []byte) ([]byte, error) { // Put implementes the Store interface. func (s *MemoryStore) Put(key, value []byte) error { - s.mem[string(key)] = value + s.mem[makeKey(key)] = value return nil } @@ -62,3 +66,7 @@ func (s *MemoryStore) Batch() Batch { m: make(map[*[]byte][]byte), } } + +func makeKey(k []byte) string { + return hex.EncodeToString(k) +} diff --git a/pkg/core/storage/memory_store_test.go b/pkg/core/storage/memory_store_test.go index 883f14cfb..1f2ba3ed1 100644 --- a/pkg/core/storage/memory_store_test.go +++ b/pkg/core/storage/memory_store_test.go @@ -22,6 +22,17 @@ func TestGetPut(t *testing.T) { 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) { var ( s = NewMemoryStore() diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 96e126dca..2d244fd9d 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -11,6 +11,7 @@ const ( DataTransaction KeyPrefix = 0x02 STAccount KeyPrefix = 0x40 STCoin KeyPrefix = 0x44 + STSpentCoin KeyPrefix = 0x45 STValidator KeyPrefix = 0x48 STAsset KeyPrefix = 0x4c STContract KeyPrefix = 0x50 diff --git a/pkg/core/transaction/enrollment.go b/pkg/core/transaction/enrollment.go new file mode 100644 index 000000000..f4295696d --- /dev/null +++ b/pkg/core/transaction/enrollment.go @@ -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) +} diff --git a/pkg/core/transaction/enrollment_test.go b/pkg/core/transaction/enrollment_test.go new file mode 100644 index 000000000..b0d04b23f --- /dev/null +++ b/pkg/core/transaction/enrollment_test.go @@ -0,0 +1,6 @@ +package transaction + +import "testing" + +func TestDecodeEncode(t *testing.T) { +} diff --git a/pkg/core/transaction/input.go b/pkg/core/transaction/input.go index 8cee4645a..a4e2ce273 100644 --- a/pkg/core/transaction/input.go +++ b/pkg/core/transaction/input.go @@ -4,7 +4,7 @@ import ( "encoding/binary" "io" - "github.com/anthdm/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/util" ) // Input represents a Transaction input. diff --git a/pkg/core/transaction/publish.go b/pkg/core/transaction/publish.go new file mode 100644 index 000000000..50a95178c --- /dev/null +++ b/pkg/core/transaction/publish.go @@ -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 +} diff --git a/pkg/core/transaction/register.go b/pkg/core/transaction/register.go index def4d023d..f7a6ccf60 100644 --- a/pkg/core/transaction/register.go +++ b/pkg/core/transaction/register.go @@ -29,7 +29,7 @@ import ( // # 2. For currencies, use only unlimited models; // # 3. For point coupons, you can use any pattern; -// RegisterTx represents a register transaction. +// RegisterTX represents a register transaction. type RegisterTX struct { // The type of the asset being registered. AssetType AssetType @@ -44,7 +44,9 @@ type RegisterTX struct { // Decimals Precision uint8 - Owner crypto.EllipticCurvePoint + // Public key of the owner + Owner *crypto.PublicKey + Admin util.Uint160 } @@ -65,11 +67,10 @@ func (tx *RegisterTX) DecodeBinary(r io.Reader) error { return err } - point, err := crypto.NewEllipticCurvePointFromReader(r) - if err != nil { + tx.Owner = &crypto.PublicKey{} + if err := tx.Owner.DecodeBinary(r); err != nil { return err } - tx.Owner = point return binary.Read(r, binary.LittleEndian, &tx.Admin) } diff --git a/pkg/core/transaction/state.go b/pkg/core/transaction/state.go new file mode 100644 index 000000000..3b61e7f84 --- /dev/null +++ b/pkg/core/transaction/state.go @@ -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 +} diff --git a/pkg/core/transaction/state_descriptor.go b/pkg/core/transaction/state_descriptor.go new file mode 100644 index 000000000..b45b250a3 --- /dev/null +++ b/pkg/core/transaction/state_descriptor.go @@ -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 +} diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index 6f13d9f8b..7141ba342 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -147,6 +147,10 @@ func (t *Transaction) decodeData(r io.Reader) error { case IssueType: t.Data = &IssueTX{} return t.Data.(*IssueTX).DecodeBinary(r) + case EnrollmentType: + t.Data = &EnrollmentTX{} + return t.Data.(*EnrollmentTX).DecodeBinary(r) + default: log.Warnf("invalid TX type %s", t.Type) } @@ -180,8 +184,10 @@ func (t *Transaction) encodeHashableFields(w io.Writer) error { } // Underlying TXer. - if err := t.Data.EncodeBinary(w); err != nil { - return err + if t.Data != nil { + if err := t.Data.EncodeBinary(w); err != nil { + return err + } } // Attributes @@ -230,3 +236,12 @@ func (t *Transaction) createHash() (hash util.Uint256, err error) { b = sha.Sum(nil) 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 +} diff --git a/pkg/core/unspent_coin_state.go b/pkg/core/unspent_coin_state.go new file mode 100644 index 000000000..53817be01 --- /dev/null +++ b/pkg/core/unspent_coin_state.go @@ -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 +} diff --git a/pkg/core/unspent_coint_state_test.go b/pkg/core/unspent_coint_state_test.go new file mode 100644 index 000000000..b02d031a4 --- /dev/null +++ b/pkg/core/unspent_coint_state_test.go @@ -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)) +} diff --git a/pkg/core/util.go b/pkg/core/util.go index faff23cd6..b5276f276 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -6,6 +6,7 @@ import ( "sort" "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" ) @@ -65,6 +66,22 @@ func storeAsBlock(batch storage.Batch, block *Block, sysFee uint32) error { 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 // retrieved from the given Store. func readStoredHeaderHashes(store storage.Store) ([]util.Uint256, error) { diff --git a/pkg/core/validator_state.go b/pkg/core/validator_state.go new file mode 100644 index 000000000..f63bf2acf --- /dev/null +++ b/pkg/core/validator_state.go @@ -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 +} diff --git a/pkg/crypto/elliptic_curve.go b/pkg/crypto/elliptic_curve.go index eaf4d46ce..cff138cb8 100644 --- a/pkg/crypto/elliptic_curve.go +++ b/pkg/crypto/elliptic_curve.go @@ -4,6 +4,8 @@ package crypto // Expanded and tweaked upon here under MIT license. import ( + "bytes" + "crypto/rand" "encoding/binary" "encoding/hex" "errors" @@ -21,51 +23,18 @@ type ( A *big.Int B *big.Int P *big.Int - G EllipticCurvePoint + G ECPoint N *big.Int H *big.Int } - // EllipticCurveEllipticCurvePoint represents a point on the EllipticCurve. - EllipticCurvePoint struct { + // ECPoint represents a point on the EllipticCurve. + ECPoint struct { X *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 // fields for the NEO protocol. func NewEllipticCurve() EllipticCurve { @@ -94,9 +63,84 @@ func NewEllipticCurve() EllipticCurve { return c } -func (p *EllipticCurvePoint) format() string { - if p.X == nil && p.Y == nil { - return "(inf,inf)" +// RandomECPoint returns a random generated ECPoint, mostly used +// for testing. +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()) 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. -func (c *EllipticCurve) IsInfinity(P EllipticCurvePoint) bool { - if P.X == nil && P.Y == nil { - return true - } - return false +func (p *ECPoint) IsInfinity() bool { + return p.X == nil && p.Y == nil +} + +// 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. -func (c *EllipticCurve) IsOnCurve(P EllipticCurvePoint) bool { +func (c *EllipticCurve) IsOnCurve(P ECPoint) bool { if c.IsInfinity(P) { return false } @@ -130,7 +176,7 @@ func (c *EllipticCurve) IsOnCurve(P EllipticCurvePoint) bool { } // 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 if c.IsInfinity(P) && c.IsInfinity(Q) { 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), P.Y, c.P) } 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 } // 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: // https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder - var R0 EllipticCurvePoint - var R1 EllipticCurvePoint + var R0 ECPoint + var R1 ECPoint R0.X = 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. -func (c *EllipticCurve) ScalarBaseMult(k *big.Int) (Q EllipticCurvePoint) { +func (c *EllipticCurve) ScalarBaseMult(k *big.Int) (Q ECPoint) { return c.ScalarMult(k, c.G) } -// Decompress decompresses coordinate x and ylsb (y's least significant bit) into a EllipticCurvePoint P on EllipticCurve ec. -func (c *EllipticCurve) Decompress(x *big.Int, ylsb uint) (P EllipticCurvePoint, err error) { +// 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 ECPoint, err error) { /* y**2 = x**3 + a*x + b % p */ rhs := addMod( addMod( diff --git a/pkg/crypto/public_key.go b/pkg/crypto/public_key.go new file mode 100644 index 000000000..e6f112579 --- /dev/null +++ b/pkg/crypto/public_key.go @@ -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()) +} diff --git a/pkg/crypto/public_key_test.go b/pkg/crypto/public_key_test.go new file mode 100644 index 000000000..37e79e1f6 --- /dev/null +++ b/pkg/crypto/public_key_test.go @@ -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) + } +} diff --git a/pkg/util/test_util.go b/pkg/util/test_util.go new file mode 100644 index 000000000..984bc60dc --- /dev/null +++ b/pkg/util/test_util.go @@ -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()) +}