From 94672cb9cc282e66fa457b12fd7524bf959b7fdd Mon Sep 17 00:00:00 2001 From: Anthony De Meulemeester Date: Sun, 25 Mar 2018 12:45:54 +0200 Subject: [PATCH] Persistance (#53) * added publish TX for backwards compat. * lowered the prototick for faster block syncing * print useragent on startup * added createMultiRedeemScript for genesis block generation. * building genesis block from scratch. * implemented merkle tree. * starting blockhain with generated genesis hash * Fixed bug in unspent coin state. * fixed broken tests after genesis block. * removed log line. * bumped version -> 0.34.0 --- VERSION | 2 +- cli/server/server.go | 45 +++---- {pkg/network => config}/config.go | 28 ++++- config/protocol.mainnet.yml | 2 +- config/protocol.privnet.yml | 2 +- config/protocol.testnet.yml | 2 +- pkg/core/account_state.go | 43 +++++-- pkg/core/account_state_test.go | 2 + pkg/core/block.go | 17 +++ pkg/core/block_base.go | 21 ++-- pkg/core/blockchain.go | 61 +++++---- pkg/core/blockchain_test.go | 21 +++- pkg/core/helper_test.go | 8 +- pkg/core/transaction/miner.go | 2 +- pkg/core/transaction/publish.go | 53 +++++++- pkg/core/transaction/register.go | 50 ++++---- pkg/core/transaction/register_test.go | 63 ++++++++++ pkg/core/transaction/transaction.go | 39 +++--- pkg/core/unspent_coin_state.go | 5 +- pkg/core/util.go | 171 ++++++++++++++++++++++++-- pkg/core/util_test.go | 61 +++++++++ pkg/crypto/elliptic_curve.go | 2 - pkg/crypto/merkle_tree.go | 99 +++++++++++++++ pkg/crypto/merkle_tree_test.go | 29 +++++ pkg/crypto/public_key.go | 49 +++++++- pkg/crypto/public_key_test.go | 21 ++++ pkg/network/message.go | 29 +---- pkg/network/server_config.go | 11 +- pkg/smartcontract/contract.go | 34 ++++- pkg/smartcontract/contract_test.go | 41 ++++++ pkg/util/fixed8.go | 16 ++- pkg/util/fixed8_test.go | 16 +++ pkg/util/io.go | 29 +++++ pkg/util/uint160.go | 19 +++ pkg/util/uint160_test.go | 49 ++++++++ 35 files changed, 955 insertions(+), 187 deletions(-) rename {pkg/network => config}/config.go (81%) create mode 100644 pkg/core/transaction/register_test.go create mode 100644 pkg/core/util_test.go create mode 100644 pkg/crypto/merkle_tree.go create mode 100644 pkg/crypto/merkle_tree_test.go create mode 100644 pkg/smartcontract/contract_test.go create mode 100644 pkg/util/fixed8_test.go diff --git a/VERSION b/VERSION index 8df3f4592..85e60ed18 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.33.1 +0.34.0 diff --git a/cli/server/server.go b/cli/server/server.go index b59ef8a32..f10b81878 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -5,11 +5,11 @@ import ( "os" "os/signal" + "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/rpc" - "github.com/CityOfZion/neo-go/pkg/util" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -32,17 +32,17 @@ func NewCommand() cli.Command { } func startServer(ctx *cli.Context) error { - net := network.ModePrivNet + net := config.ModePrivNet if ctx.Bool("testnet") { - net = network.ModeTestNet + net = config.ModeTestNet } if ctx.Bool("mainnet") { - net = network.ModeMainNet + net = config.ModeMainNet } configPath := "./config" configPath = ctx.String("config-path") - config, err := network.LoadConfig(configPath, net) + cfg, err := config.Load(configPath, net) if err != nil { return cli.NewExitError(err, 1) } @@ -50,10 +50,10 @@ func startServer(ctx *cli.Context) error { interruptChan := make(chan os.Signal, 1) signal.Notify(interruptChan, os.Interrupt) - serverConfig := network.NewServerConfig(config) - chain, err := newBlockchain(net, config.ApplicationConfiguration.DataDirectoryPath) + serverConfig := network.NewServerConfig(cfg) + chain, err := newBlockchain(cfg) if err != nil { - err = fmt.Errorf("could not initialize blockhain: %s", err) + err = fmt.Errorf("could not initialize blockchain: %s", err) return cli.NewExitError(err, 1) } @@ -61,15 +61,18 @@ func startServer(ctx *cli.Context) error { log.SetLevel(log.DebugLevel) } - fmt.Println(logo()) server := network.NewServer(serverConfig, chain) - rpcServer := rpc.NewServer(chain, config.ApplicationConfiguration.RPCPort, server) + rpcServer := rpc.NewServer(chain, cfg.ApplicationConfiguration.RPCPort, server) errChan := make(chan error) go server.Start(errChan) go rpcServer.Start(errChan) - var shutdownErr error + fmt.Println(logo()) + fmt.Println(server.UserAgent) + fmt.Println() + + var shutdownErr error Main: for { select { @@ -93,25 +96,17 @@ Main: return nil } -func newBlockchain(net network.NetMode, path string) (*core.Blockchain, error) { - var startHash util.Uint256 - if net == network.ModePrivNet { - startHash = core.GenesisHashPrivNet() - } - if net == network.ModeTestNet { - startHash = core.GenesisHashTestNet() - } - if net == network.ModeMainNet { - startHash = core.GenesisHashMainNet() - } - +func newBlockchain(cfg config.Config) (*core.Blockchain, error) { // Hardcoded for now. - store, err := storage.NewLevelDBStore(path, nil) + store, err := storage.NewLevelDBStore( + cfg.ApplicationConfiguration.DataDirectoryPath, + nil, + ) if err != nil { return nil, err } - return core.NewBlockchain(store, startHash) + return core.NewBlockchain(store, cfg.ProtocolConfiguration) } func logo() string { diff --git a/pkg/network/config.go b/config/config.go similarity index 81% rename from pkg/network/config.go rename to config/config.go index f7f8b1769..73082c89c 100644 --- a/pkg/network/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package network +package config import ( "fmt" @@ -12,6 +12,11 @@ import ( const ( userAgentFormat = "/NEO-GO:%s/" + + // Valid NetMode constants. + ModeMainNet NetMode = 0x00746e41 // 7630401 + ModeTestNet NetMode = 0x74746e41 // 1953787457 + ModePrivNet NetMode = 56753 // docker privnet ) var ( @@ -59,16 +64,33 @@ type ( ProtoTickInterval time.Duration `yaml:"ProtoTickInterval"` MaxPeers int `yaml:"MaxPeers"` } + + // NetMode describes the mode the blockchain will operate on. + NetMode uint32 ) +// String implements the stringer interface. +func (n NetMode) String() string { + switch n { + case ModePrivNet: + return "privnet" + case ModeTestNet: + return "testnet" + case ModeMainNet: + return "mainnet" + default: + return "net unknown" + } +} + // GenerateUserAgent creates user agent string based on build time environment. func (c Config) GenerateUserAgent() string { return fmt.Sprintf(userAgentFormat, Version) } -// LoadConfig attempts to load the config from the give +// Loadattempts to load the config from the give // path and netMode. -func LoadConfig(path string, netMode NetMode) (Config, error) { +func Load(path string, netMode NetMode) (Config, error) { configPath := fmt.Sprintf("%s/protocol.%s.yml", path, netMode) if _, err := os.Stat(configPath); os.IsNotExist(err) { return Config{}, errors.Wrap(err, "Unable to load config") diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index a9496485e..d59b28f40 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -27,6 +27,6 @@ ApplicationConfiguration: NodePort: 20333 Relay: true DialTimeout: 3 - ProtoTickInterval: 10 + ProtoTickInterval: 2 MaxPeers: 50 diff --git a/config/protocol.privnet.yml b/config/protocol.privnet.yml index 7183b787e..e8765d95d 100644 --- a/config/protocol.privnet.yml +++ b/config/protocol.privnet.yml @@ -23,5 +23,5 @@ ApplicationConfiguration: NodePort: 20333 Relay: true DialTimeout: 3 - ProtoTickInterval: 10 + ProtoTickInterval: 2 MaxPeers: 50 diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index 492595674..c0eac24ee 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -27,5 +27,5 @@ ApplicationConfiguration: NodePort: 20333 Relay: true DialTimeout: 3 - ProtoTickInterval: 10 + ProtoTickInterval: 2 MaxPeers: 50 diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go index f32bb0bd6..c22fa7380 100644 --- a/pkg/core/account_state.go +++ b/pkg/core/account_state.go @@ -3,6 +3,7 @@ package core import ( "bytes" "encoding/binary" + "fmt" "io" "github.com/CityOfZion/neo-go/pkg/core/storage" @@ -18,11 +19,11 @@ func (a Accounts) getAndChange(s storage.Store, hash util.Uint160) (*AccountStat return account, nil } - var account *AccountState + 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 + return nil, fmt.Errorf("failed to decode (AccountState): %s", err) } } else { account = NewAccountState(hash) @@ -48,6 +49,7 @@ func (a Accounts) commit(b storage.Batch) error { // AccountState represents the state of a NEO account. type AccountState struct { + Version uint8 ScriptHash util.Uint160 IsFrozen bool Votes []*crypto.PublicKey @@ -57,6 +59,7 @@ type AccountState struct { // NewAccountState returns a new AccountState object. func NewAccountState(scriptHash util.Uint160) *AccountState { return &AccountState{ + Version: 0, ScriptHash: scriptHash, IsFrozen: false, Votes: []*crypto.PublicKey{}, @@ -66,6 +69,9 @@ func NewAccountState(scriptHash util.Uint160) *AccountState { // DecodeBinary decodes AccountState from the given io.Reader. func (s *AccountState) DecodeBinary(r io.Reader) error { + if err := binary.Read(r, binary.LittleEndian, &s.Version); err != nil { + return err + } if err := binary.Read(r, binary.LittleEndian, &s.ScriptHash); err != nil { return err } @@ -101,6 +107,9 @@ func (s *AccountState) DecodeBinary(r io.Reader) error { // EncodeBinary encode AccountState to the given io.Writer. func (s *AccountState) EncodeBinary(w io.Writer) error { + if err := binary.Write(w, binary.LittleEndian, s.Version); err != nil { + return err + } if err := binary.Write(w, binary.LittleEndian, s.ScriptHash); err != nil { return err } @@ -111,27 +120,35 @@ func (s *AccountState) EncodeBinary(w io.Writer) error { 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 { + balances := s.nonZeroBalances() + if err := util.WriteVarUint(w, uint64(len(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 - } + for k, v := range balances { + 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 } + +// Returns only the non-zero balances for the account. +func (s *AccountState) nonZeroBalances() map[util.Uint256]util.Fixed8 { + b := make(map[util.Uint256]util.Fixed8) + for k, v := range s.Balances { + if v > 0 { + b[k] = v + } + } + return b +} diff --git a/pkg/core/account_state_test.go b/pkg/core/account_state_test.go index 043891da8..e2e4c4e00 100644 --- a/pkg/core/account_state_test.go +++ b/pkg/core/account_state_test.go @@ -21,6 +21,7 @@ func TestDecodeEncodeAccountState(t *testing.T) { } a := &AccountState{ + Version: 0, ScriptHash: util.RandomUint160(), IsFrozen: true, Votes: votes, @@ -37,6 +38,7 @@ func TestDecodeEncodeAccountState(t *testing.T) { t.Fatal(err) } + assert.Equal(t, a.Version, aDecode.Version) assert.Equal(t, a.ScriptHash, aDecode.ScriptHash) assert.Equal(t, a.IsFrozen, aDecode.IsFrozen) diff --git a/pkg/core/block.go b/pkg/core/block.go index 81b6018db..762ba6787 100644 --- a/pkg/core/block.go +++ b/pkg/core/block.go @@ -6,6 +6,7 @@ import ( "io" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/util" log "github.com/sirupsen/logrus" ) @@ -29,6 +30,22 @@ func (b *Block) Header() *Header { } } +// rebuildMerkleRoot rebuild the merkleroot of the block. +func (b *Block) rebuildMerkleRoot() error { + hashes := make([]util.Uint256, len(b.Transactions)) + for i, tx := range b.Transactions { + hashes[i] = tx.Hash() + } + + merkle, err := crypto.NewMerkleTree(hashes) + if err != nil { + return err + } + + b.MerkleRoot = merkle.Root() + return nil +} + // Verify the integrity of the block. func (b *Block) Verify(full bool) bool { // The first TX has to be a miner transaction. diff --git a/pkg/core/block_base.go b/pkg/core/block_base.go index 45db3776b..fc993e6f2 100644 --- a/pkg/core/block_base.go +++ b/pkg/core/block_base.go @@ -54,6 +54,9 @@ func (b *BlockBase) Verify() bool { // Hash return the hash of the block. func (b *BlockBase) Hash() util.Uint256 { + if b.hash.Equals(util.Uint256{}) { + b.createHash() + } return b.hash } @@ -92,16 +95,19 @@ func (b *BlockBase) EncodeBinary(w io.Writer) error { // version, PrevBlock, MerkleRoot, timestamp, and height, the nonce, NextMiner. // Since MerkleRoot already contains the hash value of all transactions, // the modification of transaction will influence the hash value of the block. -func (b *BlockBase) createHash() (hash util.Uint256, err error) { +func (b *BlockBase) createHash() error { buf := new(bytes.Buffer) - if err = b.encodeHashableFields(buf); err != nil { - return hash, err + if err := b.encodeHashableFields(buf); err != nil { + return err } // Double hash the encoded fields. + var hash util.Uint256 hash = sha256.Sum256(buf.Bytes()) hash = sha256.Sum256(hash.Bytes()) - return hash, nil + b.hash = hash + + return nil } // encodeHashableFields will only encode the fields used for hashing. @@ -155,10 +161,5 @@ func (b *BlockBase) decodeHashableFields(r io.Reader) error { // Make the hash of the block here so we dont need to do this // again. - hash, err := b.createHash() - if err != nil { - return err - } - b.hash = hash - return nil + return b.createHash() } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index b64de75a8..f91dde66c 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -4,10 +4,10 @@ import ( "bytes" "encoding/binary" "fmt" - "strings" "sync/atomic" "time" + "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" @@ -18,15 +18,19 @@ import ( const ( secondsPerBlock = 15 headerBatchCount = 2000 + version = "0.0.1" ) var ( - genAmount = []int{8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} - persistInterval = 5 * time.Second + genAmount = []int{8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + decrementInterval = 2000000 + persistInterval = 5 * time.Second ) -// Blockchain holds the chain. +// Blockchain represents the blockchain. type Blockchain struct { + config config.ProtocolConfiguration + // Any object that satisfies the BlockchainStorer interface. storage.Store @@ -40,8 +44,6 @@ type Blockchain struct { blockCache *Cache - startHash util.Uint256 - // All operation on headerList must be called from an // headersOp to be routine safe. headerList *HeaderHashList @@ -58,12 +60,12 @@ type headersOpFunc func(headerList *HeaderHashList) // NewBlockchain return a new blockchain object the will use the // given Store as its underlying storage. -func NewBlockchain(s storage.Store, startHash util.Uint256) (*Blockchain, error) { +func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration) (*Blockchain, error) { bc := &Blockchain{ + config: cfg, Store: s, headersOp: make(chan headersOpFunc), headersOpDone: make(chan struct{}), - startHash: startHash, blockCache: NewCache(), verifyBlocks: false, } @@ -77,19 +79,30 @@ func NewBlockchain(s storage.Store, startHash util.Uint256) (*Blockchain, error) } func (bc *Blockchain) init() error { - // TODO: This should be the persistance of the genisis block. - // for now we just add the genisis block start hash. - bc.headerList = NewHeaderHashList(bc.startHash) - bc.storedHeaderCount = 1 // genisis hash + genesisBlock, err := createGenesisBlock(bc.config) + if err != nil { + return err + } + bc.headerList = NewHeaderHashList(genesisBlock.Hash()) + + // Look in the storage for a version. If we could not the version key + // there is nothing stored. + if version, err := bc.Get(storage.SYSVersion.Bytes()); err != nil { + bc.Put(storage.SYSVersion.Bytes(), []byte(version)) + if err := bc.persistBlock(genesisBlock); err != nil { + return err + } + + return nil + } + + // At this point there was no version found in the storage which + // implies a creating fresh storage with the version specified + // and the genesis block as first block. + log.Infof("restoring blockchain with storage version: %s", version) - // If we get an "not found" error, the store could not find - // the current block, which indicates there is nothing stored - // in the chain file. currBlockBytes, err := bc.Get(storage.SYSCurrentBlock.Bytes()) if err != nil { - if strings.Contains(err.Error(), "not found") { - return nil - } return err } @@ -98,8 +111,9 @@ func (bc *Blockchain) init() error { if err != nil { return err } + for _, hash := range hashes { - if !bc.startHash.Equals(hash) { + if !genesisBlock.Hash().Equals(hash) { bc.headerList.Add(hash) bc.storedHeaderCount++ } @@ -292,7 +306,7 @@ func (bc *Blockchain) persistBlock(block *Block) error { } if output.AssetID.Equals(bc.governingToken()) && len(account.Votes) > 0 { - log.Warnf("governing token detected in TX output need to update validators!") + // TODO } } @@ -300,7 +314,7 @@ func (bc *Blockchain) persistBlock(block *Block) error { for prevHash, inputs := range tx.GroupInputsByPrevHash() { prevTX, _, err := bc.GetTransaction(prevHash) if err != nil { - return err + return fmt.Errorf("could not find previous TX: %s", prevHash) } for _, input := range inputs { unspent, err := unspentCoins.getAndChange(bc.Store, input.PrevHash) @@ -316,7 +330,7 @@ func (bc *Blockchain) persistBlock(block *Block) error { } if prevTXOutput.AssetID.Equals(bc.governingToken()) { - log.Warnf("governing token detected in TX input need to update validators!") + // TODO } account.Balances[prevTXOutput.AssetID] -= prevTXOutput.Amount @@ -347,7 +361,7 @@ func (bc *Blockchain) persistBlock(block *Block) error { return err } - atomic.AddUint32(&bc.blockHeight, 1) + atomic.StoreUint32(&bc.blockHeight, block.Index) return nil } @@ -366,6 +380,7 @@ func (bc *Blockchain) persist() (err error) { hash := headerList.Get(int(bc.BlockHeight() + 1)) if block, ok := bc.blockCache.GetBlock(hash); ok { if err = bc.persistBlock(block); err != nil { + log.Warnf("failed to persist blocks: %s", err) return } bc.blockCache.Delete(hash) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 109a557a7..8d4842de1 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -3,8 +3,8 @@ package core import ( "testing" + "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core/storage" - "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,6 @@ func TestAddHeaders(t *testing.T) { assert.Equal(t, 0, bc.blockCache.Len()) assert.Equal(t, h3.Index, bc.HeaderHeight()) - assert.Equal(t, uint32(1), bc.storedHeaderCount) assert.Equal(t, uint32(0), bc.BlockHeight()) assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) @@ -30,7 +29,6 @@ func TestAddHeaders(t *testing.T) { } assert.Equal(t, h3.Index, bc.HeaderHeight()) - assert.Equal(t, uint32(1), bc.storedHeaderCount) assert.Equal(t, uint32(0), bc.BlockHeight()) assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash()) } @@ -53,12 +51,20 @@ func TestAddBlock(t *testing.T) { assert.Equal(t, 3, bc.blockCache.Len()) assert.Equal(t, lastBlock.Index, bc.HeaderHeight()) assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) - assert.Equal(t, uint32(1), bc.storedHeaderCount) + + t.Log(bc.blockCache) if err := bc.persist(); err != nil { t.Fatal(err) } + for _, block := range blocks { + key := storage.AppendPrefix(storage.DataBlock, block.Hash().BytesReverse()) + if _, err := bc.Get(key); err != nil { + t.Fatalf("block %s not persisted", block.Hash()) + } + } + assert.Equal(t, lastBlock.Index, bc.BlockHeight()) assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash()) assert.Equal(t, 0, bc.blockCache.Len()) @@ -138,8 +144,11 @@ func TestGetTransaction(t *testing.T) { } func newTestChain(t *testing.T) *Blockchain { - startHash, _ := util.Uint256DecodeString("a") - chain, err := NewBlockchain(storage.NewMemoryStore(), startHash) + cfg, err := config.Load("../../config", config.ModePrivNet) + if err != nil { + t.Fatal(err) + } + chain, err := NewBlockchain(storage.NewMemoryStore(), cfg.ProtocolConfiguration) if err != nil { t.Fatal(err) } diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index f6681cf79..febbee30d 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -31,11 +31,9 @@ func newBlock(index uint32, txs ...*transaction.Transaction) *Block { }, Transactions: txs, } - hash, err := b.createHash() - if err != nil { - panic(err) - } - b.hash = hash + + b.createHash() + return b } diff --git a/pkg/core/transaction/miner.go b/pkg/core/transaction/miner.go index af7284b93..3fa8075e9 100644 --- a/pkg/core/transaction/miner.go +++ b/pkg/core/transaction/miner.go @@ -7,7 +7,7 @@ import ( // MinerTX represents a miner transaction. type MinerTX struct { - // Random number/identifier + // Random number to avoid hash collision. Nonce uint32 } diff --git a/pkg/core/transaction/publish.go b/pkg/core/transaction/publish.go index 50a95178c..ed92409c9 100644 --- a/pkg/core/transaction/publish.go +++ b/pkg/core/transaction/publish.go @@ -1,19 +1,22 @@ package transaction import ( + "encoding/binary" "io" "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" ) // PublishTX represents a publish transaction. -// This is deprecated and should no longer be used. +// NOTE: This is deprecated and should no longer be used. type PublishTX struct { Script []byte ParamList []smartcontract.ParamType ReturnType smartcontract.ParamType NeedStorage bool Name string + CodeVersion string Author string Email string Description string @@ -21,6 +24,54 @@ type PublishTX struct { // DecodeBinary implements the Payload interface. func (tx *PublishTX) DecodeBinary(r io.Reader) error { + var err error + + tx.Script, err = util.ReadVarBytes(r) + if err != nil { + return err + } + + lenParams := util.ReadVarUint(r) + tx.ParamList = make([]smartcontract.ParamType, lenParams) + for i := 0; i < int(lenParams); i++ { + var ptype uint8 + if err := binary.Read(r, binary.LittleEndian, &ptype); err != nil { + return err + } + tx.ParamList[i] = smartcontract.ParamType(ptype) + } + + var rtype uint8 + if err := binary.Read(r, binary.LittleEndian, &rtype); err != nil { + return err + } + tx.ReturnType = smartcontract.ParamType(rtype) + + if err := binary.Read(r, binary.LittleEndian, &tx.NeedStorage); err != nil { + return err + } + + tx.Name, err = util.ReadVarString(r) + if err != nil { + return err + } + tx.CodeVersion, err = util.ReadVarString(r) + if err != nil { + return err + } + tx.Author, err = util.ReadVarString(r) + if err != nil { + return err + } + tx.Email, err = util.ReadVarString(r) + if err != nil { + return err + } + tx.Description, err = util.ReadVarString(r) + if err != nil { + return err + } + return nil } diff --git a/pkg/core/transaction/register.go b/pkg/core/transaction/register.go index f7a6ccf60..423c33e44 100644 --- a/pkg/core/transaction/register.go +++ b/pkg/core/transaction/register.go @@ -8,34 +8,14 @@ import ( "github.com/CityOfZion/neo-go/pkg/util" ) -// # 发行总量,共有2种模式: -// # 1. 限量模式:当Amount为正数时,表示当前资产的最大总量为Amount,且不可修改(股权在未来可能会支持扩股或增发,会考虑需要公司签名或一定比例的股东签名认可)。 -// # 2. 不限量模式:当Amount等于-1时,表示当前资产可以由创建者无限量发行。这种模式的自由度最大,但是公信力最低,不建议使用。 -// # 在使用过程中,根据资产类型的不同,能够使用的总量模式也不同,具体规则如下: -// # 1. 对于股权,只能使用限量模式; -// # 2. 对于货币,只能使用不限量模式; -// # 3. 对于点券,可以使用任意模式; -// -// In English: -// # Total number of releases, there are 2 modes: -// # 1. Limited amount: When Amount is positive, it means that the maximum amount of current assets is Amount -// and can not be modified (the equity may support the expansion or issuance in the future, will consider the -// need for company signature or a certain percentage of shareholder signature recognition ). -// # 2. Unlimited mode: When Amount is equal to -1, it means that the current asset can be issued by the -// creator unlimited. This mode of freedom is the largest, but the credibility of the lowest, not recommended. -// # In the use of the process, according to the different types of assets, can use the total amount of -// different models, the specific rules are as follows: -// # 1. For equity, use only limited models; -// # 2. For currencies, use only unlimited models; -// # 3. For point coupons, you can use any pattern; - // RegisterTX represents a register transaction. +// NOTE: This is deprecated. type RegisterTX struct { // The type of the asset being registered. AssetType AssetType // Name of the asset being registered. - Name []byte + Name string // Amount registered // Unlimited mode -0.00000001 @@ -55,14 +35,17 @@ func (tx *RegisterTX) DecodeBinary(r io.Reader) error { if err := binary.Read(r, binary.LittleEndian, &tx.AssetType); err != nil { return err } - lenName := util.ReadVarUint(r) - tx.Name = make([]byte, lenName) - if err := binary.Read(r, binary.LittleEndian, &tx.Name); err != nil { + + var err error + tx.Name, err = util.ReadVarString(r) + if err != nil { return err } + if err := binary.Read(r, binary.LittleEndian, &tx.Amount); err != nil { return err } + if err := binary.Read(r, binary.LittleEndian, &tx.Precision); err != nil { return err } @@ -77,5 +60,20 @@ func (tx *RegisterTX) DecodeBinary(r io.Reader) error { // EncodeBinary implements the Payload interface. func (tx *RegisterTX) EncodeBinary(w io.Writer) error { - return nil + if err := binary.Write(w, binary.LittleEndian, tx.AssetType); err != nil { + return err + } + if err := util.WriteVarString(w, tx.Name); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, tx.Amount); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, tx.Precision); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, tx.Owner.Bytes()); err != nil { + return err + } + return binary.Write(w, binary.LittleEndian, tx.Admin) } diff --git a/pkg/core/transaction/register_test.go b/pkg/core/transaction/register_test.go new file mode 100644 index 000000000..b9d83e078 --- /dev/null +++ b/pkg/core/transaction/register_test.go @@ -0,0 +1,63 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/assert" +) + +func TestRegisterTX(t *testing.T) { + tx := &Transaction{ + Type: RegisterType, + Version: 0, + Data: &RegisterTX{ + AssetType: UtilityToken, + Name: "this is some token I created", + Amount: util.NewFixed8(1000000), + Precision: 8, + Owner: &crypto.PublicKey{}, + Admin: util.RandomUint160(), + }, + } + + buf := new(bytes.Buffer) + assert.Nil(t, tx.EncodeBinary(buf)) + + txDecode := &Transaction{} + assert.Nil(t, txDecode.DecodeBinary(buf)) + txData := tx.Data.(*RegisterTX) + txDecodeData := txDecode.Data.(*RegisterTX) + assert.Equal(t, txData, txDecodeData) + assert.Equal(t, tx.Hash(), txDecode.Hash()) +} + +func TestDecodeRegisterTXFromRawString(t *testing.T) { + rawTX := "400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000" + b, err := hex.DecodeString(rawTX) + if err != nil { + t.Fatal(err) + } + + tx := &Transaction{} + assert.Nil(t, tx.DecodeBinary(bytes.NewReader(b))) + assert.Equal(t, RegisterType, tx.Type) + txData := tx.Data.(*RegisterTX) + assert.Equal(t, GoverningToken, txData.AssetType) + assert.Equal(t, "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"AntShare\"}]", txData.Name) + assert.Equal(t, util.NewFixed8(100000000), txData.Amount) + assert.Equal(t, uint8(0), txData.Precision) + assert.Equal(t, &crypto.PublicKey{}, txData.Owner) + assert.Equal(t, "Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt", crypto.AddressFromUint160(txData.Admin)) + assert.Equal(t, "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", tx.Hash().String()) + + buf := new(bytes.Buffer) + assert.Nil(t, tx.EncodeBinary(buf)) + + txDecode := &Transaction{} + assert.Nil(t, txDecode.DecodeBinary(buf)) + assert.Equal(t, tx, txDecode) +} diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index f80fabc8f..50b42210d 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -55,6 +55,9 @@ func NewTrimmedTX(hash util.Uint256) *Transaction { // Hash return the hash of the transaction. func (t *Transaction) Hash() util.Uint256 { + if t.hash.Equals(util.Uint256{}) { + t.createHash() + } return t.hash } @@ -118,13 +121,7 @@ func (t *Transaction) DecodeBinary(r io.Reader) error { // Create the hash of the transaction at decode, so we dont need // to do it anymore. - hash, err := t.createHash() - if err != nil { - return err - } - t.hash = hash - - return nil + return t.createHash() } func (t *Transaction) decodeData(r io.Reader) error { @@ -150,7 +147,12 @@ func (t *Transaction) decodeData(r io.Reader) error { case EnrollmentType: t.Data = &EnrollmentTX{} return t.Data.(*EnrollmentTX).DecodeBinary(r) - + case PublishType: + t.Data = &PublishTX{} + return t.Data.(*PublishTX).DecodeBinary(r) + case StateType: + t.Data = &StateTX{} + return t.Data.(*StateTX).DecodeBinary(r) default: log.Warnf("invalid TX type %s", t.Type) } @@ -223,18 +225,19 @@ func (t *Transaction) encodeHashableFields(w io.Writer) error { return nil } -func (t *Transaction) createHash() (hash util.Uint256, err error) { +// createHash creates the hash of the transaction. +func (t *Transaction) createHash() error { buf := new(bytes.Buffer) - if err = t.encodeHashableFields(buf); err != nil { - return + if err := t.encodeHashableFields(buf); err != nil { + return err } - sha := sha256.New() - sha.Write(buf.Bytes()) - b := sha.Sum(nil) - sha.Reset() - sha.Write(b) - b = sha.Sum(nil) - return util.Uint256DecodeBytes(util.ArrayReverse(b)) + + var hash util.Uint256 + hash = sha256.Sum256(buf.Bytes()) + hash = sha256.Sum256(hash.Bytes()) + t.hash = hash + + return nil } // GroupTXInputsByPrevHash groups all TX inputs by their previous hash. diff --git a/pkg/core/unspent_coin_state.go b/pkg/core/unspent_coin_state.go index 53817be01..1f0c7af17 100644 --- a/pkg/core/unspent_coin_state.go +++ b/pkg/core/unspent_coin_state.go @@ -3,6 +3,7 @@ package core import ( "bytes" "encoding/binary" + "fmt" "io" "github.com/CityOfZion/neo-go/pkg/core/storage" @@ -18,11 +19,11 @@ func (u UnspentCoins) getAndChange(s storage.Store, hash util.Uint256) (*Unspent return unspent, nil } - var unspent *UnspentCoinState + 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 + return nil, fmt.Errorf("failed to decode (UnspentCoinState): %s", err) } } else { unspent = &UnspentCoinState{ diff --git a/pkg/core/util.go b/pkg/core/util.go index b5276f276..66753c40c 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -4,29 +4,176 @@ import ( "bytes" "encoding/binary" "sort" + "time" + "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" ) -// Utilities for quick bootstrapping blockchains. Normally we should -// create the genisis block. For now (to speed up development) we will add -// The hashes manually. +// Creates a genesis block based on the given configuration. +func createGenesisBlock(cfg config.ProtocolConfiguration) (*Block, error) { + validators, err := getValidators(cfg) + if err != nil { + return nil, err + } -func GenesisHashPrivNet() util.Uint256 { - hash, _ := util.Uint256DecodeString("996e37358dc369912041f966f8c5d8d3a8255ba5dcbd3447f8a82b55db869099") - return hash + nextConsensus, err := getNextConsensusAddress(validators) + if err != nil { + return nil, err + } + + base := BlockBase{ + Version: 0, + PrevHash: util.Uint256{}, + Timestamp: uint32(time.Date(2016, 7, 15, 15, 8, 21, 0, time.UTC).Unix()), + Index: 0, + ConsensusData: 2083236893, + NextConsensus: nextConsensus, + Script: &transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: []byte{byte(vm.Opusht)}, + }, + } + + governingTX := governingTokenTX() + utilityTX := utilityTokenTX() + rawScript, err := smartcontract.CreateMultiSigRedeemScript( + len(cfg.StandbyValidators)/2+1, + validators, + ) + if err != nil { + return nil, err + } + scriptOut, err := util.Uint160FromScript(rawScript) + if err != nil { + return nil, err + } + + block := &Block{ + BlockBase: base, + Transactions: []*transaction.Transaction{ + { + Type: transaction.MinerType, + Data: &transaction.MinerTX{ + Nonce: 2083236893, + }, + Attributes: []*transaction.Attribute{}, + Inputs: []*transaction.Input{}, + Outputs: []*transaction.Output{}, + Scripts: []*transaction.Witness{}, + }, + governingTX, + utilityTX, + { + Type: transaction.IssueType, + Data: &transaction.IssueTX{}, // no fields. + Inputs: []*transaction.Input{}, + Outputs: []*transaction.Output{ + { + AssetID: governingTX.Hash(), + Amount: governingTX.Data.(*transaction.RegisterTX).Amount, + ScriptHash: scriptOut, + }, + }, + Scripts: []*transaction.Witness{ + { + InvocationScript: []byte{}, + VerificationScript: []byte{byte(vm.Opusht)}, + }, + }, + }, + }, + } + + block.rebuildMerkleRoot() + + return block, nil } -func GenesisHashTestNet() util.Uint256 { - hash, _ := util.Uint256DecodeString("b3181718ef6167105b70920e4a8fbbd0a0a56aacf460d70e10ba6fa1668f1fef") - return hash +func governingTokenTX() *transaction.Transaction { + admin, _ := util.Uint160FromScript([]byte{byte(vm.Opusht)}) + adminB, _ := crypto.Uint160DecodeAddress("Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt") + if !admin.Equals(adminB) { + panic("kdjdkljfkdjfkdjf") + } + registerTX := &transaction.RegisterTX{ + AssetType: transaction.GoverningToken, + Name: "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"AntShare\"}]", + Amount: util.NewFixed8(100000000), + Precision: 0, + Owner: &crypto.PublicKey{}, + Admin: admin, + } + + tx := &transaction.Transaction{ + Type: transaction.RegisterType, + Data: registerTX, + Attributes: []*transaction.Attribute{}, + Inputs: []*transaction.Input{}, + Outputs: []*transaction.Output{}, + Scripts: []*transaction.Witness{}, + } + + return tx } -func GenesisHashMainNet() util.Uint256 { - hash, _ := util.Uint256DecodeString("d42561e3d30e15be6400b6df2f328e02d2bf6354c41dce433bc57687c82144bf") - return hash +func utilityTokenTX() *transaction.Transaction { + admin, _ := util.Uint160FromScript([]byte{byte(vm.Opushf)}) + registerTX := &transaction.RegisterTX{ + AssetType: transaction.UtilityToken, + Name: "[{\"lang\":\"zh-CN\",\"name\":\"小蚁币\"},{\"lang\":\"en\",\"name\":\"AntCoin\"}]", + Amount: calculateUtilityAmount(), + Precision: 8, + Owner: &crypto.PublicKey{}, + Admin: admin, + } + tx := &transaction.Transaction{ + Type: transaction.RegisterType, + Data: registerTX, + Attributes: []*transaction.Attribute{}, + Inputs: []*transaction.Input{}, + Outputs: []*transaction.Output{}, + Scripts: []*transaction.Witness{}, + } + + return tx +} + +func getValidators(cfg config.ProtocolConfiguration) ([]*crypto.PublicKey, error) { + validators := make([]*crypto.PublicKey, len(cfg.StandbyValidators)) + for i, pubKeyStr := range cfg.StandbyValidators { + pubKey, err := crypto.NewPublicKeyFromString(pubKeyStr) + if err != nil { + return nil, err + } + validators[i] = pubKey + } + return validators, nil +} + +func getNextConsensusAddress(validators []*crypto.PublicKey) (val util.Uint160, err error) { + vlen := len(validators) + raw, err := smartcontract.CreateMultiSigRedeemScript( + vlen-(vlen-1)/3, + validators, + ) + if err != nil { + return val, err + } + return util.Uint160FromScript(raw) +} + +func calculateUtilityAmount() util.Fixed8 { + sum := 0 + for i := 0; i < len(genAmount); i++ { + sum += genAmount[i] + } + return util.NewFixed8(sum * decrementInterval) } // headerSliceReverse reverses the given slice of *Header. diff --git a/pkg/core/util_test.go b/pkg/core/util_test.go new file mode 100644 index 000000000..4d48bb359 --- /dev/null +++ b/pkg/core/util_test.go @@ -0,0 +1,61 @@ +package core + +import ( + "testing" + + "github.com/CityOfZion/neo-go/config" + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/stretchr/testify/assert" +) + +func TestGenesisBlockMainNet(t *testing.T) { + cfg, err := config.Load("../../config", config.ModeMainNet) + if err != nil { + t.Fatal(err) + } + + block, err := createGenesisBlock(cfg.ProtocolConfiguration) + if err != nil { + t.Fatal(err) + } + + expect := "d42561e3d30e15be6400b6df2f328e02d2bf6354c41dce433bc57687c82144bf" + assert.Equal(t, expect, block.Hash().String()) +} + +func TestGetConsensusAddressMainNet(t *testing.T) { + var ( + consensusAddr = "APyEx5f4Zm4oCHwFWiSTaph1fPBxZacYVR" + consensusScript = "59e75d652b5d3827bf04c165bbe9ef95cca4bf55" + ) + + cfg, err := config.Load("../../config", config.ModeMainNet) + if err != nil { + t.Fatal(err) + } + + validators, err := getValidators(cfg.ProtocolConfiguration) + if err != nil { + t.Fatal(err) + } + + script, err := getNextConsensusAddress(validators) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, consensusScript, script.String()) + assert.Equal(t, consensusAddr, crypto.AddressFromUint160(script)) +} + +func TestUtilityTokenTX(t *testing.T) { + expect := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7" + tx := utilityTokenTX() + assert.Equal(t, expect, tx.Hash().String()) +} + +func TestGoverningTokenTX(t *testing.T) { + expect := "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b" + tx := governingTokenTX() + assert.Equal(t, expect, tx.Hash().String()) +} diff --git a/pkg/crypto/elliptic_curve.go b/pkg/crypto/elliptic_curve.go index cff138cb8..6a42d1167 100644 --- a/pkg/crypto/elliptic_curve.go +++ b/pkg/crypto/elliptic_curve.go @@ -88,8 +88,6 @@ func ECPointFromReader(r io.Reader) (point ECPoint, err error) { return } - fmt.Println(f) - // Infinity if f == 0 { return ECPoint{ diff --git a/pkg/crypto/merkle_tree.go b/pkg/crypto/merkle_tree.go new file mode 100644 index 000000000..1226b9def --- /dev/null +++ b/pkg/crypto/merkle_tree.go @@ -0,0 +1,99 @@ +package crypto + +import ( + "crypto/sha256" + "errors" + + "github.com/CityOfZion/neo-go/pkg/util" +) + +// MerkleTree implementation. + +type MerkleTree struct { + root *MerkleTreeNode + depth int +} + +// NewMerkleTree returns new MerkleTree object. +func NewMerkleTree(hashes []util.Uint256) (*MerkleTree, error) { + if len(hashes) == 0 { + return nil, errors.New("length of the hashes cannot be zero") + } + + nodes := make([]*MerkleTreeNode, len(hashes)) + for i := 0; i < len(hashes); i++ { + nodes[i] = &MerkleTreeNode{ + hash: hashes[i], + } + } + + root, err := buildMerkleTree(nodes) + if err != nil { + return nil, err + } + + return &MerkleTree{ + root: root, + depth: 1, + }, nil +} + +// Root return the computed root hash of the MerkleTree. +func (t *MerkleTree) Root() util.Uint256 { + return t.root.hash +} + +func buildMerkleTree(leaves []*MerkleTreeNode) (*MerkleTreeNode, error) { + if len(leaves) == 0 { + return nil, errors.New("length of the leaves cannot be zero") + } + if len(leaves) == 1 { + return leaves[0], nil + } + + parents := make([]*MerkleTreeNode, (len(leaves)+1)/2) + for i := 0; i < len(parents); i++ { + parents[i] = &MerkleTreeNode{} + parents[i].leftChild = leaves[i*2] + leaves[i*2].parent = parents[i] + + if i*2+1 == len(leaves) { + parents[i].rightChild = parents[i].leftChild + } else { + parents[i].rightChild = leaves[i*2+1] + leaves[i*2+1].parent = parents[i] + } + + b1 := parents[i].leftChild.hash.Bytes() + b2 := parents[i].rightChild.hash.Bytes() + b1 = append(b1, b2...) + parents[i].hash = hash256(b1) + } + + return buildMerkleTree(parents) +} + +// MerkleTreeNode represents a node in the MerkleTree. +type MerkleTreeNode struct { + hash util.Uint256 + parent *MerkleTreeNode + leftChild *MerkleTreeNode + rightChild *MerkleTreeNode +} + +// IsLeaf returns whether this node is a leaf node or not. +func (n *MerkleTreeNode) IsLeaf() bool { + return n.leftChild == nil && n.rightChild == nil +} + +// IsRoot returns whether this node is a root node or not. +func (n *MerkleTreeNode) IsRoot() bool { + return n.parent == nil +} + +func hash256(b []byte) util.Uint256 { + var hash util.Uint256 + hash = sha256.Sum256(b) + hash = sha256.Sum256(hash.Bytes()) + return hash +} diff --git a/pkg/crypto/merkle_tree_test.go b/pkg/crypto/merkle_tree_test.go new file mode 100644 index 000000000..4be94f786 --- /dev/null +++ b/pkg/crypto/merkle_tree_test.go @@ -0,0 +1,29 @@ +package crypto + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/assert" +) + +func TestComputeMerkleTree(t *testing.T) { + rawHashes := []string{ + "fb5bd72b2d6792d75dc2f1084ffa9e9f70ca85543c717a6b13d9959b452a57d6", + "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", + "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + "3631f66024ca6f5b033d7e0809eb993443374830025af904fb51b0334f127cda", + } + + hashes := make([]util.Uint256, len(rawHashes)) + for i, str := range rawHashes { + hash, _ := util.Uint256DecodeString(str) + hashes[i] = hash + } + + merkle, err := NewMerkleTree(hashes) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "803ff4abe3ea6533bcc0be574efa02f83ae8fdc651c879056b0d9be336c01bf4", merkle.Root().String()) +} diff --git a/pkg/crypto/public_key.go b/pkg/crypto/public_key.go index e6f112579..060df90e2 100644 --- a/pkg/crypto/public_key.go +++ b/pkg/crypto/public_key.go @@ -3,18 +3,59 @@ package crypto import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" "io" "math/big" ) -// PublicKey represents a public key. +// PublicKeys is a list of public keys. +type PublicKeys []*PublicKey + +func (keys PublicKeys) Len() int { return len(keys) } +func (keys PublicKeys) Swap(i, j int) { keys[i], keys[j] = keys[j], keys[i] } +func (keys PublicKeys) Less(i, j int) bool { + if keys[i].X.Cmp(keys[j].X) == -1 { + return true + } + if keys[i].X.Cmp(keys[j].X) == 1 { + return false + } + if keys[i].X.Cmp(keys[j].X) == 0 { + return false + } + + return keys[i].Y.Cmp(keys[j].Y) == -1 +} + +// PublicKey represents a public key and provides a high level +// API around the ECPoint. type PublicKey struct { ECPoint } +// NewPublicKeyFromString return a public key created from the +// given hex string. +func NewPublicKeyFromString(s string) (*PublicKey, error) { + b, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + + pubKey := &PublicKey{} + if err := pubKey.DecodeBinary(bytes.NewReader(b)); err != nil { + return nil, err + } + + return pubKey, nil +} + // Bytes returns the byte array representation of the public key. func (p *PublicKey) Bytes() []byte { + if p.IsInfinity() { + return []byte{0x00} + } + var ( x = p.X.Bytes() paddedX = append(bytes.Repeat([]byte{0x00}, 32-len(x)), x...) @@ -35,6 +76,12 @@ func (p *PublicKey) DecodeBinary(r io.Reader) error { return err } + // Infinity + if prefix == 0x00 { + p.ECPoint = ECPoint{} + return nil + } + // Compressed public keys. if prefix == 0x02 || prefix == 0x03 { c := NewEllipticCurve() diff --git a/pkg/crypto/public_key_test.go b/pkg/crypto/public_key_test.go index 37e79e1f6..99c809c1b 100644 --- a/pkg/crypto/public_key_test.go +++ b/pkg/crypto/public_key_test.go @@ -2,11 +2,23 @@ package crypto import ( "bytes" + "encoding/hex" "testing" "github.com/stretchr/testify/assert" ) +func TestEncodeDecodeInfinity(t *testing.T) { + key := &PublicKey{ECPoint{}} + buf := new(bytes.Buffer) + assert.Nil(t, key.EncodeBinary(buf)) + assert.Equal(t, 1, buf.Len()) + + keyDecode := &PublicKey{} + assert.Nil(t, keyDecode.DecodeBinary(buf)) + assert.Equal(t, []byte{0x00}, keyDecode.Bytes()) +} + func TestEncodeDecodePublicKey(t *testing.T) { for i := 0; i < 4; i++ { p := &PublicKey{RandomECPoint()} @@ -18,3 +30,12 @@ func TestEncodeDecodePublicKey(t *testing.T) { assert.Equal(t, p.X, pDecode.X) } } + +func TestDecodeFromString(t *testing.T) { + str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" + pubKey, err := NewPublicKeyFromString(str) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, str, hex.EncodeToString(pubKey.Bytes())) +} diff --git a/pkg/network/message.go b/pkg/network/message.go index 2db9d505a..fc1ed497e 100644 --- a/pkg/network/message.go +++ b/pkg/network/message.go @@ -8,6 +8,7 @@ import ( "fmt" "io" + "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/network/payload" @@ -23,34 +24,10 @@ var ( errChecksumMismatch = errors.New("checksum mismatch") ) -// NetMode type that is compatible with netModes below. -type NetMode uint32 - -// String implements the stringer interface. -func (n NetMode) String() string { - switch n { - case ModePrivNet: - return "privnet" - case ModeTestNet: - return "testnet" - case ModeMainNet: - return "mainnet" - default: - return "net unknown" - } -} - -// Values used for the magic field, according to the docs. -const ( - ModeMainNet NetMode = 0x00746e41 // 7630401 - ModeTestNet NetMode = 0x74746e41 // 1953787457 - ModePrivNet NetMode = 56753 // docker privnet -) - // Message is the complete message send between nodes. type Message struct { // NetMode of the node that sends this message. - Magic NetMode + Magic config.NetMode // Command is utf8 code, of which the length is 12 bytes, // the extra part is filled with 0. @@ -88,7 +65,7 @@ const ( ) // NewMessage returns a new message with the given payload. -func NewMessage(magic NetMode, cmd CommandType, p payload.Payload) *Message { +func NewMessage(magic config.NetMode, cmd CommandType, p payload.Payload) *Message { var ( size uint32 checksum []byte diff --git a/pkg/network/server_config.go b/pkg/network/server_config.go index 46052590b..fd0c2b249 100644 --- a/pkg/network/server_config.go +++ b/pkg/network/server_config.go @@ -3,6 +3,7 @@ package network import ( "time" + "github.com/CityOfZion/neo-go/config" log "github.com/sirupsen/logrus" ) @@ -23,7 +24,7 @@ type ( // ModePrivNet docker private network. // ModeTestNet NEO test network. // ModeMainNet NEO main network. - Net NetMode + Net config.NetMode // Relay determins whether the server is forwarding its inventory. Relay bool @@ -45,12 +46,12 @@ type ( // NewServerConfig creates a new ServerConfig struct // using the main applications config. -func NewServerConfig(config Config) ServerConfig { - appConfig := config.ApplicationConfiguration - protoConfig := config.ProtocolConfiguration +func NewServerConfig(cfg config.Config) ServerConfig { + appConfig := cfg.ApplicationConfiguration + protoConfig := cfg.ProtocolConfiguration return ServerConfig{ - UserAgent: config.GenerateUserAgent(), + UserAgent: cfg.GenerateUserAgent(), ListenTCP: appConfig.NodePort, Net: protoConfig.Magic, Relay: appConfig.Relay, diff --git a/pkg/smartcontract/contract.go b/pkg/smartcontract/contract.go index 526ea36fb..82e821d76 100644 --- a/pkg/smartcontract/contract.go +++ b/pkg/smartcontract/contract.go @@ -1,4 +1,34 @@ package smartcontract -// Contract represents a NEO smartcontract. -type Contract struct{} +import ( + "bytes" + "fmt" + "sort" + + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/vm" +) + +// CreateMultiSigRedeemScript will create a script runnable by the VM. +func CreateMultiSigRedeemScript(m int, publicKeys crypto.PublicKeys) ([]byte, error) { + if m <= 1 { + return nil, fmt.Errorf("param m cannot be smaller or equal to 1 got %d", m) + } + if m > len(publicKeys) { + return nil, fmt.Errorf("length of the signatures (%d) is higher then the number of public keys", m) + } + if m > 1024 { + return nil, fmt.Errorf("public key count %d exceeds maximum of length 1024", len(publicKeys)) + } + + buf := new(bytes.Buffer) + vm.EmitInt(buf, int64(m)) + sort.Sort(publicKeys) + for _, pubKey := range publicKeys { + vm.EmitBytes(buf, pubKey.Bytes()) + } + vm.EmitInt(buf, int64(len(publicKeys))) + vm.EmitOpcode(buf, vm.Ocheckmultisig) + + return buf.Bytes(), nil +} diff --git a/pkg/smartcontract/contract_test.go b/pkg/smartcontract/contract_test.go new file mode 100644 index 000000000..d8760743c --- /dev/null +++ b/pkg/smartcontract/contract_test.go @@ -0,0 +1,41 @@ +package smartcontract + +import ( + "bytes" + "testing" + + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/stretchr/testify/assert" +) + +func TestCreateMultiSigRedeemScript(t *testing.T) { + val1, _ := crypto.NewPublicKeyFromString("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c") + val2, _ := crypto.NewPublicKeyFromString("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093") + val3, _ := crypto.NewPublicKeyFromString("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a") + + validators := []*crypto.PublicKey{val1, val2, val3} + + out, err := CreateMultiSigRedeemScript(3, validators) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(out) + b, _ := buf.ReadByte() + assert.Equal(t, vm.Opush3, vm.Opcode(b)) + + for i := 0; i < len(validators); i++ { + b, err := util.ReadVarBytes(buf) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, validators[i].Bytes(), b) + } + + b, _ = buf.ReadByte() + assert.Equal(t, vm.Opush3, vm.Opcode(b)) + b, _ = buf.ReadByte() + assert.Equal(t, vm.Ocheckmultisig, vm.Opcode(b)) +} diff --git a/pkg/util/fixed8.go b/pkg/util/fixed8.go index 22dd8429f..de03cc03d 100644 --- a/pkg/util/fixed8.go +++ b/pkg/util/fixed8.go @@ -5,6 +5,8 @@ import ( "strconv" ) +const decimals = 100000000 + // Fixed8 represents a fixed-point number with precision 10^-8. type Fixed8 int64 @@ -16,9 +18,9 @@ func (f Fixed8) String() string { buf.WriteRune('-') val = -val } - str := strconv.FormatInt(val/100000000, 10) + str := strconv.FormatInt(val/decimals, 10) buf.WriteString(str) - val %= 100000000 + val %= decimals if val > 0 { buf.WriteRune('.') str = strconv.FormatInt(val, 10) @@ -29,3 +31,13 @@ func (f Fixed8) String() string { } return buf.String() } + +// Value returns the original value representing the Fixed8. +func (f Fixed8) Value() int64 { + return int64(f) / int64(decimals) +} + +// NewFixed8 return a new Fixed8 type multiplied by decimals. +func NewFixed8(val int) Fixed8 { + return Fixed8(decimals * val) +} diff --git a/pkg/util/fixed8_test.go b/pkg/util/fixed8_test.go new file mode 100644 index 000000000..13fbeac49 --- /dev/null +++ b/pkg/util/fixed8_test.go @@ -0,0 +1,16 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewFixed8(t *testing.T) { + values := []int{9000, 100000000, 5, 10945} + + for _, val := range values { + assert.Equal(t, Fixed8(val*decimals), NewFixed8(val)) + assert.Equal(t, int64(val), NewFixed8(val).Value()) + } +} diff --git a/pkg/util/io.go b/pkg/util/io.go index 3568d809a..41eb82416 100644 --- a/pkg/util/io.go +++ b/pkg/util/io.go @@ -65,6 +65,35 @@ func WriteVarUint(w io.Writer, val uint64) error { return nil } +// ReadVarBytes reads a variable length byte array. +func ReadVarBytes(r io.Reader) ([]byte, error) { + n := ReadVarUint(r) + b := make([]byte, n) + if err := binary.Read(r, binary.LittleEndian, b); err != nil { + return nil, err + } + return b, nil +} + +// ReadVarString reads a variable length string. +func ReadVarString(r io.Reader) (string, error) { + b, err := ReadVarBytes(r) + return string(b), err +} + +// WriteVarString writes a variable length string. +func WriteVarString(w io.Writer, s string) error { + return WriteVarBytes(w, []byte(s)) +} + +// WriteVarBytes writes a variable length byte array. +func WriteVarBytes(w io.Writer, b []byte) error { + if err := WriteVarUint(w, uint64(len(b))); err != nil { + return err + } + return binary.Write(w, binary.LittleEndian, b) +} + // Read2000Uint256Hashes attempt to read 2000 Uint256 hashes from // the given byte array. func Read2000Uint256Hashes(b []byte) ([]Uint256, error) { diff --git a/pkg/util/uint160.go b/pkg/util/uint160.go index 88f51f879..0e093d3f1 100644 --- a/pkg/util/uint160.go +++ b/pkg/util/uint160.go @@ -1,9 +1,12 @@ package util import ( + "crypto/sha256" "encoding/hex" "encoding/json" "fmt" + + "golang.org/x/crypto/ripemd160" ) const uint160Size = 20 @@ -34,6 +37,17 @@ func Uint160DecodeBytes(b []byte) (u Uint160, err error) { return } +// Uint160FromScript returns a Uint160 type from a raw script. +func Uint160FromScript(script []byte) (u Uint160, err error) { + sha := sha256.New() + sha.Write(script) + b := sha.Sum(nil) + ripemd := ripemd160.New() + ripemd.Write(b) + b = ripemd.Sum(nil) + return Uint160DecodeBytes(b) +} + // Bytes returns the byte slice representation of u. func (u Uint160) Bytes() []byte { b := make([]byte, uint160Size) @@ -43,6 +57,11 @@ func (u Uint160) Bytes() []byte { return b } +// BytesReverse return a reversed byte representation of u. +func (u Uint160) BytesReverse() []byte { + return ArrayReverse(u.Bytes()) +} + // String implements the stringer interface. func (u Uint160) String() string { return hex.EncodeToString(u.Bytes()) diff --git a/pkg/util/uint160_test.go b/pkg/util/uint160_test.go index c7d868219..96416653c 100644 --- a/pkg/util/uint160_test.go +++ b/pkg/util/uint160_test.go @@ -1 +1,50 @@ package util + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUInt160DecodeString(t *testing.T) { + hexStr := "2d3b96ae1bcc5a585e075e3b81920210dec16302" + val, err := Uint160DecodeString(hexStr) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, hexStr, val.String()) +} + +func TestUint160DecodeBytes(t *testing.T) { + hexStr := "2d3b96ae1bcc5a585e075e3b81920210dec16302" + b, err := hex.DecodeString(hexStr) + if err != nil { + t.Fatal(err) + } + val, err := Uint160DecodeBytes(b) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, hexStr, val.String()) +} + +func TestUInt160Equals(t *testing.T) { + a := "2d3b96ae1bcc5a585e075e3b81920210dec16302" + b := "4d3b96ae1bcc5a585e075e3b81920210dec16302" + + ua, err := Uint160DecodeString(a) + if err != nil { + t.Fatal(err) + } + ub, err := Uint160DecodeString(b) + if err != nil { + t.Fatal(err) + } + if ua.Equals(ub) { + t.Fatalf("%s and %s cannot be equal", ua, ub) + } + if !ua.Equals(ua) { + t.Fatalf("%s and %s must be equal", ua, ua) + } +}