From add9368e9d40daa899e31c7a53a584cc9a020852 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 7 Oct 2019 17:05:53 +0300 Subject: [PATCH 01/31] storage: use strings as keys for memory batch Using pointers is just plain wrong here, because the batch can be updated with newer values for the same keys. Fixes Seek() to use HasPrefix also because this is the intended behavior. --- pkg/core/storage/boltdb_store.go | 2 +- pkg/core/storage/boltdb_store_test.go | 15 -------------- pkg/core/storage/memory_store.go | 28 ++++++++++----------------- pkg/core/storage/redis_store.go | 2 +- pkg/core/storage/redis_store_test.go | 10 +--------- 5 files changed, 13 insertions(+), 44 deletions(-) diff --git a/pkg/core/storage/boltdb_store.go b/pkg/core/storage/boltdb_store.go index 18283bc63..e2c6c52a0 100644 --- a/pkg/core/storage/boltdb_store.go +++ b/pkg/core/storage/boltdb_store.go @@ -76,7 +76,7 @@ func (s *BoltDBStore) PutBatch(batch Batch) error { return s.db.Batch(func(tx *bbolt.Tx) error { b := tx.Bucket(Bucket) for k, v := range batch.(*MemoryBatch).m { - err := b.Put(*k, v) + err := b.Put([]byte(k), v) if err != nil { return err } diff --git a/pkg/core/storage/boltdb_store_test.go b/pkg/core/storage/boltdb_store_test.go index e5cf397b1..299a74816 100644 --- a/pkg/core/storage/boltdb_store_test.go +++ b/pkg/core/storage/boltdb_store_test.go @@ -3,27 +3,12 @@ package storage import ( "io/ioutil" "os" - "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestBoltDBBatch(t *testing.T) { - boltDB := BoltDBStore{} - want := &MemoryBatch{m: map[*[]byte][]byte{}} - if got := boltDB.Batch(); !reflect.DeepEqual(got, want) { - t.Errorf("BoltDB Batch() = %v, want %v", got, want) - } -} - -func TestBoltDBBatch_Len(t *testing.T) { - batch := &MemoryBatch{m: map[*[]byte][]byte{}} - want := len(map[*[]byte][]byte{}) - assert.Equal(t, want, batch.Len()) -} - func TestBoltDBBatch_PutBatchAndGet(t *testing.T) { key := []byte("foo") keycopy := make([]byte, len(key)) diff --git a/pkg/core/storage/memory_store.go b/pkg/core/storage/memory_store.go index 789dffdbb..8ce29c7db 100644 --- a/pkg/core/storage/memory_store.go +++ b/pkg/core/storage/memory_store.go @@ -1,7 +1,6 @@ package storage import ( - "encoding/hex" "strings" "sync" ) @@ -15,16 +14,15 @@ type MemoryStore struct { // MemoryBatch a in-memory batch compatible with MemoryStore. type MemoryBatch struct { - m map[*[]byte][]byte + m map[string][]byte } // Put implements the Batch interface. func (b *MemoryBatch) Put(k, v []byte) { vcopy := make([]byte, len(v)) copy(vcopy, v) - kcopy := make([]byte, len(k)) - copy(kcopy, k) - b.m[&kcopy] = vcopy + kcopy := string(k) + b.m[kcopy] = vcopy } // Len implements the Batch interface. @@ -43,7 +41,7 @@ func NewMemoryStore() *MemoryStore { func (s *MemoryStore) Get(key []byte) ([]byte, error) { s.mut.RLock() defer s.mut.RUnlock() - if val, ok := s.mem[makeKey(key)]; ok { + if val, ok := s.mem[string(key)]; ok { return val, nil } return nil, ErrKeyNotFound @@ -52,7 +50,7 @@ func (s *MemoryStore) Get(key []byte) ([]byte, error) { // Put implements the Store interface. Never returns an error. func (s *MemoryStore) Put(key, value []byte) error { s.mut.Lock() - s.mem[makeKey(key)] = value + s.mem[string(key)] = value s.mut.Unlock() return nil } @@ -61,7 +59,7 @@ func (s *MemoryStore) Put(key, value []byte) error { func (s *MemoryStore) PutBatch(batch Batch) error { b := batch.(*MemoryBatch) for k, v := range b.m { - _ = s.Put(*k, v) + _ = s.Put([]byte(k), v) } return nil } @@ -69,9 +67,8 @@ func (s *MemoryStore) PutBatch(batch Batch) error { // Seek implements the Store interface. func (s *MemoryStore) Seek(key []byte, f func(k, v []byte)) { for k, v := range s.mem { - if strings.Contains(k, hex.EncodeToString(key)) { - decodeString, _ := hex.DecodeString(k) - f(decodeString, v) + if strings.HasPrefix(k, string(key)) { + f([]byte(k), v) } } } @@ -84,7 +81,7 @@ func (s *MemoryStore) Batch() Batch { // newMemoryBatch returns new memory batch. func newMemoryBatch() *MemoryBatch { return &MemoryBatch{ - m: make(map[*[]byte][]byte), + m: make(map[string][]byte), } } @@ -96,8 +93,7 @@ func (s *MemoryStore) Persist(ps Store) (int, error) { batch := ps.Batch() keys := 0 for k, v := range s.mem { - kb, _ := hex.DecodeString(k) - batch.Put(kb, v) + batch.Put([]byte(k), v) keys++ } var err error @@ -118,7 +114,3 @@ func (s *MemoryStore) Close() error { s.mut.Unlock() return nil } - -func makeKey(k []byte) string { - return hex.EncodeToString(k) -} diff --git a/pkg/core/storage/redis_store.go b/pkg/core/storage/redis_store.go index 5dd5ac4e0..30021c9fd 100644 --- a/pkg/core/storage/redis_store.go +++ b/pkg/core/storage/redis_store.go @@ -58,7 +58,7 @@ func (s *RedisStore) Put(k, v []byte) error { func (s *RedisStore) PutBatch(b Batch) error { pipe := s.client.Pipeline() for k, v := range b.(*MemoryBatch).m { - pipe.Set(string(*k), v, 0) + pipe.Set(k, v, 0) } _, err := pipe.Exec() return err diff --git a/pkg/core/storage/redis_store_test.go b/pkg/core/storage/redis_store_test.go index f60f76922..919c5683f 100644 --- a/pkg/core/storage/redis_store_test.go +++ b/pkg/core/storage/redis_store_test.go @@ -23,14 +23,6 @@ func TestNewRedisStore(t *testing.T) { redisMock.Close() } -func TestRedisBatch_Len(t *testing.T) { - want := len(map[string]string{}) - b := &MemoryBatch{ - m: map[*[]byte][]byte{}, - } - assert.Equal(t, len(b.m), want) -} - func TestRedisStore_GetAndPut(t *testing.T) { prepareRedisMock(t) type args struct { @@ -82,7 +74,7 @@ func TestRedisStore_GetAndPut(t *testing.T) { } func TestRedisStore_PutBatch(t *testing.T) { - batch := &MemoryBatch{m: map[*[]byte][]byte{&[]byte{'f', 'o', 'o', '1'}: []byte("bar1")}} + batch := &MemoryBatch{m: map[string][]byte{"foo1": []byte("bar1")}} mock, redisStore := prepareRedisMock(t) err := redisStore.PutBatch(batch) assert.Nil(t, err, "Error while PutBatch") From 48b6a427cfb5498d84e29a5dcf66a6610820ba94 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 7 Oct 2019 17:08:09 +0300 Subject: [PATCH 02/31] storage: add Delete method for Batch and Store It's gonna be used by Storage and Contract interops, both can delete their data. --- pkg/core/storage/boltdb_store.go | 14 +++++++++ pkg/core/storage/leveldb_store.go | 5 ++++ pkg/core/storage/memory_store.go | 43 ++++++++++++++++++++++++--- pkg/core/storage/memory_store_test.go | 12 ++++++++ pkg/core/storage/redis_store.go | 9 ++++++ pkg/core/storage/store.go | 2 ++ 6 files changed, 81 insertions(+), 4 deletions(-) diff --git a/pkg/core/storage/boltdb_store.go b/pkg/core/storage/boltdb_store.go index e2c6c52a0..dc435a72c 100644 --- a/pkg/core/storage/boltdb_store.go +++ b/pkg/core/storage/boltdb_store.go @@ -71,6 +71,14 @@ func (s *BoltDBStore) Get(key []byte) (val []byte, err error) { return } +// Delete implements the Store interface. +func (s *BoltDBStore) Delete(key []byte) error { + return s.db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(Bucket) + return b.Delete(key) + }) +} + // PutBatch implements the Store interface. func (s *BoltDBStore) PutBatch(batch Batch) error { return s.db.Batch(func(tx *bbolt.Tx) error { @@ -81,6 +89,12 @@ func (s *BoltDBStore) PutBatch(batch Batch) error { return err } } + for k := range batch.(*MemoryBatch).del { + err := b.Delete([]byte(k)) + if err != nil { + return err + } + } return nil }) } diff --git a/pkg/core/storage/leveldb_store.go b/pkg/core/storage/leveldb_store.go index f024c41d3..b2637744e 100644 --- a/pkg/core/storage/leveldb_store.go +++ b/pkg/core/storage/leveldb_store.go @@ -48,6 +48,11 @@ func (s *LevelDBStore) Get(key []byte) ([]byte, error) { return value, err } +// Delete implements the Store interface. +func (s *LevelDBStore) Delete(key []byte) error { + return s.db.Delete(key, nil) +} + // PutBatch implements the Store interface. func (s *LevelDBStore) PutBatch(batch Batch) error { lvldbBatch := batch.(*leveldb.Batch) diff --git a/pkg/core/storage/memory_store.go b/pkg/core/storage/memory_store.go index 8ce29c7db..571ab64e5 100644 --- a/pkg/core/storage/memory_store.go +++ b/pkg/core/storage/memory_store.go @@ -10,11 +10,15 @@ import ( type MemoryStore struct { mut sync.RWMutex mem map[string][]byte + // A map, not a slice, to avoid duplicates. + del map[string]bool } // MemoryBatch a in-memory batch compatible with MemoryStore. type MemoryBatch struct { m map[string][]byte + // A map, not a slice, to avoid duplicates. + del map[string]bool } // Put implements the Batch interface. @@ -23,6 +27,14 @@ func (b *MemoryBatch) Put(k, v []byte) { copy(vcopy, v) kcopy := string(k) b.m[kcopy] = vcopy + delete(b.del, kcopy) +} + +// Delete implements Batch interface. +func (b *MemoryBatch) Delete(k []byte) { + kcopy := string(k) + delete(b.m, kcopy) + b.del[kcopy] = true } // Len implements the Batch interface. @@ -34,6 +46,7 @@ func (b *MemoryBatch) Len() int { func NewMemoryStore() *MemoryStore { return &MemoryStore{ mem: make(map[string][]byte), + del: make(map[string]bool), } } @@ -50,7 +63,19 @@ func (s *MemoryStore) Get(key []byte) ([]byte, error) { // Put implements the Store interface. Never returns an error. func (s *MemoryStore) Put(key, value []byte) error { s.mut.Lock() - s.mem[string(key)] = value + newKey := string(key) + s.mem[newKey] = value + delete(s.del, newKey) + s.mut.Unlock() + return nil +} + +// Delete implements Store interface. Never returns an error. +func (s *MemoryStore) Delete(key []byte) error { + s.mut.Lock() + newKey := string(key) + s.del[newKey] = true + delete(s.mem, newKey) s.mut.Unlock() return nil } @@ -58,6 +83,9 @@ func (s *MemoryStore) Put(key, value []byte) error { // PutBatch implements the Store interface. Never returns an error. func (s *MemoryStore) PutBatch(batch Batch) error { b := batch.(*MemoryBatch) + for k := range b.del { + _ = s.Delete([]byte(k)) + } for k, v := range b.m { _ = s.Put([]byte(k), v) } @@ -81,7 +109,8 @@ func (s *MemoryStore) Batch() Batch { // newMemoryBatch returns new memory batch. func newMemoryBatch() *MemoryBatch { return &MemoryBatch{ - m: make(map[string][]byte), + m: make(map[string][]byte), + del: make(map[string]bool), } } @@ -91,17 +120,22 @@ func (s *MemoryStore) Persist(ps Store) (int, error) { s.mut.Lock() defer s.mut.Unlock() batch := ps.Batch() - keys := 0 + keys, dkeys := 0, 0 for k, v := range s.mem { batch.Put([]byte(k), v) keys++ } + for k := range s.del { + batch.Delete([]byte(k)) + dkeys++ + } var err error - if keys != 0 { + if keys != 0 || dkeys != 0 { err = ps.PutBatch(batch) } if err == nil { s.mem = make(map[string][]byte) + s.del = make(map[string]bool) } return keys, err } @@ -110,6 +144,7 @@ func (s *MemoryStore) Persist(ps Store) (int, error) { // error. func (s *MemoryStore) Close() error { s.mut.Lock() + s.del = nil s.mem = nil s.mut.Unlock() return nil diff --git a/pkg/core/storage/memory_store_test.go b/pkg/core/storage/memory_store_test.go index 30aa526fd..96a7f5da5 100644 --- a/pkg/core/storage/memory_store_test.go +++ b/pkg/core/storage/memory_store_test.go @@ -125,4 +125,16 @@ func TestMemoryStorePersist(t *testing.T) { c, err = ts.Persist(ps) assert.Equal(t, nil, err) assert.Equal(t, 0, c) + // test persisting deletions + err = ts.Delete([]byte("key")) + assert.Equal(t, nil, err) + c, err = ts.Persist(ps) + assert.Equal(t, nil, err) + assert.Equal(t, 0, c) + v, err = ps.Get([]byte("key")) + assert.Equal(t, ErrKeyNotFound, err) + assert.Equal(t, []byte(nil), v) + v, err = ps.Get([]byte("key2")) + assert.Equal(t, nil, err) + assert.Equal(t, []byte("value2"), v) } diff --git a/pkg/core/storage/redis_store.go b/pkg/core/storage/redis_store.go index 30021c9fd..0d8ef8e74 100644 --- a/pkg/core/storage/redis_store.go +++ b/pkg/core/storage/redis_store.go @@ -48,6 +48,12 @@ func (s *RedisStore) Get(k []byte) ([]byte, error) { return []byte(val), nil } +// Delete implements the Store interface. +func (s *RedisStore) Delete(k []byte) error { + s.client.Del(string(k)) + return nil +} + // Put implements the Store interface. func (s *RedisStore) Put(k, v []byte) error { s.client.Set(string(k), string(v), 0) @@ -60,6 +66,9 @@ func (s *RedisStore) PutBatch(b Batch) error { for k, v := range b.(*MemoryBatch).m { pipe.Set(k, v, 0) } + for k := range b.(*MemoryBatch).del { + pipe.Del(k) + } _, err := pipe.Exec() return err } diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 92fdeab07..a9bbacd5b 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -32,6 +32,7 @@ type ( // information. Store interface { Batch() Batch + Delete(k []byte) error Get([]byte) ([]byte, error) Put(k, v []byte) error PutBatch(Batch) error @@ -43,6 +44,7 @@ type ( // Each Store implementation is responsible of casting a Batch // to its appropriate type. Batch interface { + Delete(k []byte) Put(k, v []byte) Len() int } From e111892653ea1a068733cba0ebb4bf64a551a31b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 7 Oct 2019 19:45:01 +0300 Subject: [PATCH 03/31] storage: redo DB testing Make generic tests for all Store implementations, deduplicate tests. Implement Delete() tests, test Seek() better, add LevelDB tests (finally!). --- pkg/core/storage/boltdb_store_test.go | 58 +----- pkg/core/storage/leveldb_store_test.go | 40 +++++ pkg/core/storage/memory_store_test.go | 74 +------- pkg/core/storage/redis_store_test.go | 110 ++---------- pkg/core/storage/storeandbatch_test.go | 239 +++++++++++++++++++++++++ 5 files changed, 302 insertions(+), 219 deletions(-) create mode 100644 pkg/core/storage/storeandbatch_test.go diff --git a/pkg/core/storage/boltdb_store_test.go b/pkg/core/storage/boltdb_store_test.go index 299a74816..e9759b299 100644 --- a/pkg/core/storage/boltdb_store_test.go +++ b/pkg/core/storage/boltdb_store_test.go @@ -5,66 +5,10 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestBoltDBBatch_PutBatchAndGet(t *testing.T) { - key := []byte("foo") - keycopy := make([]byte, len(key)) - copy(keycopy, key) - value := []byte("bar") - valuecopy := make([]byte, len(value)) - copy(valuecopy, value) - boltDBStore := openStore(t) - batch := boltDBStore.Batch() - - batch.Put(keycopy, valuecopy) - copy(valuecopy, key) - copy(keycopy, value) - errPut := boltDBStore.PutBatch(batch) - assert.Nil(t, errPut, "Error while PutBatch") - - result, err := boltDBStore.Get(key) - assert.Nil(t, err) - assert.Equal(t, value, result) - - require.NoError(t, boltDBStore.Close()) -} - -func TestBoltDBBatch_PutAndGet(t *testing.T) { - key := []byte("foo") - value := []byte("bar") - - boltDBStore := openStore(t) - - errPut := boltDBStore.Put(key, value) - assert.Nil(t, errPut, "Error while Put") - - result, err := boltDBStore.Get(key) - assert.Nil(t, err) - assert.Equal(t, value, result) - - require.NoError(t, boltDBStore.Close()) -} - -func TestBoltDBStore_Seek(t *testing.T) { - key := []byte("foo") - value := []byte("bar") - - boltDBStore := openStore(t) - - errPut := boltDBStore.Put(key, value) - assert.Nil(t, errPut, "Error while Put") - - boltDBStore.Seek(key, func(k, v []byte) { - assert.Equal(t, value, v) - }) - - require.NoError(t, boltDBStore.Close()) -} - -func openStore(t *testing.T) *BoltDBStore { +func newBoltStoreForTesting(t *testing.T) Store { testFileName := "test_bolt_db" file, err := ioutil.TempFile("", testFileName) defer func() { diff --git a/pkg/core/storage/leveldb_store_test.go b/pkg/core/storage/leveldb_store_test.go index 82be0547e..b8dcbbe83 100644 --- a/pkg/core/storage/leveldb_store_test.go +++ b/pkg/core/storage/leveldb_store_test.go @@ -1 +1,41 @@ package storage + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +type tempLevelDB struct { + LevelDBStore + dir string +} + +func (tldb *tempLevelDB) Close() error { + err := tldb.LevelDBStore.Close() + // Make test fail if failed to cleanup, even though technically it's + // not a LevelDBStore problem. + osErr := os.RemoveAll(tldb.dir) + if osErr != nil { + return osErr + } + return err +} + +func newLevelDBForTesting(t *testing.T) Store { + ldbDir, err := ioutil.TempDir(os.TempDir(), "testleveldb") + require.Nil(t, err, "failed to setup temporary directory") + + dbConfig := DBConfiguration{ + Type: "leveldb", + LevelDBOptions: LevelDBOptions{ + DataDirectoryPath: ldbDir, + }, + } + newLevelStore, err := NewLevelDBStore(dbConfig.LevelDBOptions) + require.Nil(t, err, "NewLevelDBStore error") + tldb := &tempLevelDB{LevelDBStore: *newLevelStore, dir: ldbDir} + return tldb +} diff --git a/pkg/core/storage/memory_store_test.go b/pkg/core/storage/memory_store_test.go index 96a7f5da5..1cf9f6d9a 100644 --- a/pkg/core/storage/memory_store_test.go +++ b/pkg/core/storage/memory_store_test.go @@ -4,78 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestGetPut(t *testing.T) { - var ( - s = NewMemoryStore() - key = []byte("sparse") - value = []byte("rocks") - ) - - if err := s.Put(key, value); err != nil { - t.Fatal(err) - } - - newVal, err := s.Get(key) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, value, newVal) - require.NoError(t, s.Close()) -} - -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") - require.NoError(t, s.Close()) -} - -func TestPutBatch(t *testing.T) { - var ( - s = NewMemoryStore() - key = []byte("sparse") - value = []byte("rocks") - batch = s.Batch() - ) - - batch.Put(key, value) - - if err := s.PutBatch(batch); err != nil { - t.Fatal(err) - } - - newVal, err := s.Get(key) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, value, newVal) - require.NoError(t, s.Close()) -} - -func TestMemoryStore_Seek(t *testing.T) { - var ( - s = NewMemoryStore() - key = []byte("sparse") - value = []byte("rocks") - ) - - if err := s.Put(key, value); err != nil { - t.Fatal(err) - } - - s.Seek(key, func(k, v []byte) { - assert.Equal(t, value, v) - }) -} - func TestMemoryStorePersist(t *testing.T) { // temporary Store ts := NewMemoryStore() @@ -138,3 +68,7 @@ func TestMemoryStorePersist(t *testing.T) { assert.Equal(t, nil, err) assert.Equal(t, []byte("value2"), v) } + +func newMemoryStoreForTesting(t *testing.T) Store { + return NewMemoryStore() +} diff --git a/pkg/core/storage/redis_store_test.go b/pkg/core/storage/redis_store_test.go index 919c5683f..5c82c506c 100644 --- a/pkg/core/storage/redis_store_test.go +++ b/pkg/core/storage/redis_store_test.go @@ -4,101 +4,18 @@ import ( "testing" "github.com/alicebob/miniredis" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestNewRedisStore(t *testing.T) { - redisMock, redisStore := prepareRedisMock(t) - key := []byte("testKey") - value := []byte("testValue") - err := redisStore.Put(key, value) - assert.Nil(t, err, "NewRedisStore Put error") - - result, err := redisStore.Get(key) - assert.Nil(t, err, "NewRedisStore Get error") - - assert.Equal(t, value, result) - require.NoError(t, redisStore.Close()) - redisMock.Close() -} - -func TestRedisStore_GetAndPut(t *testing.T) { - prepareRedisMock(t) - type args struct { - k []byte - v []byte - kToLook []byte - } - tests := []struct { - name string - args args - want []byte - wantErr bool - }{ - {"TestRedisStore_Get_Strings", - args{ - k: []byte("foo"), - v: []byte("bar"), - kToLook: []byte("foo"), - }, - []byte("bar"), - false, - }, - {"TestRedisStore_Get_Negative_Strings", - args{ - k: []byte("foo"), - v: []byte("bar"), - kToLook: []byte("wrong"), - }, - []byte(nil), - true, - }, - } - redisMock, redisStore := prepareRedisMock(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := redisStore.Put(tt.args.k, tt.args.v) - assert.Nil(t, err, "Got error while Put operation processing") - got, err := redisStore.Get(tt.args.kToLook) - if (err != nil) != tt.wantErr { - t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) - return - } - assert.Equal(t, tt.want, got) - redisMock.FlushDB() - }) - } - require.NoError(t, redisStore.Close()) - redisMock.Close() -} - -func TestRedisStore_PutBatch(t *testing.T) { - batch := &MemoryBatch{m: map[string][]byte{"foo1": []byte("bar1")}} - mock, redisStore := prepareRedisMock(t) - err := redisStore.PutBatch(batch) - assert.Nil(t, err, "Error while PutBatch") - result, err := redisStore.Get([]byte("foo1")) - assert.Nil(t, err) - assert.Equal(t, []byte("bar1"), result) - require.NoError(t, redisStore.Close()) - mock.Close() -} - -func TestRedisStore_Seek(t *testing.T) { - mock, redisStore := prepareRedisMock(t) - redisStore.Seek([]byte("foo"), func(k, v []byte) { - assert.Equal(t, []byte("bar"), v) - }) - require.NoError(t, redisStore.Close()) - mock.Close() +type mockedRedisStore struct { + RedisStore + mini *miniredis.Miniredis } func prepareRedisMock(t *testing.T) (*miniredis.Miniredis, *RedisStore) { miniRedis, err := miniredis.Run() - if err != nil { - t.Errorf("MiniRedis mock creation error = %v", err) - } + require.Nil(t, err, "MiniRedis mock creation error") + _ = miniRedis.Set("foo", "bar") dbConfig := DBConfiguration{ @@ -110,9 +27,18 @@ func prepareRedisMock(t *testing.T) (*miniredis.Miniredis, *RedisStore) { }, } newRedisStore, err := NewRedisStore(dbConfig.RedisDBOptions) - if err != nil { - t.Errorf("NewRedisStore() error = %v", err) - return nil, nil - } + require.Nil(t, err, "NewRedisStore() error") return miniRedis, newRedisStore } + +func (mrs *mockedRedisStore) Close() error { + err := mrs.RedisStore.Close() + mrs.mini.Close() + return err +} + +func newRedisStoreForTesting(t *testing.T) Store { + mock, rs := prepareRedisMock(t) + mrs := &mockedRedisStore{RedisStore: *rs, mini: mock} + return mrs +} diff --git a/pkg/core/storage/storeandbatch_test.go b/pkg/core/storage/storeandbatch_test.go new file mode 100644 index 000000000..76ecbe754 --- /dev/null +++ b/pkg/core/storage/storeandbatch_test.go @@ -0,0 +1,239 @@ +package storage + +import ( + "reflect" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type dbSetup struct { + name string + create func(*testing.T) Store +} + +type dbTestFunction func(*testing.T, Store) + +func testStoreClose(t *testing.T, s Store) { + require.NoError(t, s.Close()) +} + +func testStorePutAndGet(t *testing.T, s Store) { + key := []byte("foo") + value := []byte("bar") + + require.NoError(t, s.Put(key, value)) + + result, err := s.Get(key) + assert.Nil(t, err) + require.Equal(t, value, result) + + require.NoError(t, s.Close()) +} + +func testStoreGetNonExistent(t *testing.T, s Store) { + key := []byte("sparse") + + _, err := s.Get(key) + assert.Equal(t, err, ErrKeyNotFound) + require.NoError(t, s.Close()) +} + +func testStorePutBatch(t *testing.T, s Store) { + var ( + key = []byte("foo") + value = []byte("bar") + batch = s.Batch() + ) + // Test that key and value are copied when batching. + keycopy := make([]byte, len(key)) + copy(keycopy, key) + valuecopy := make([]byte, len(value)) + copy(valuecopy, value) + + batch.Put(keycopy, valuecopy) + copy(valuecopy, key) + copy(keycopy, value) + + require.NoError(t, s.PutBatch(batch)) + newVal, err := s.Get(key) + assert.Nil(t, err) + require.Equal(t, value, newVal) + assert.Equal(t, value, newVal) + require.NoError(t, s.Close()) +} + +func testStoreSeek(t *testing.T, s Store) { + type kvSeen struct { + key []byte + val []byte + seen bool + } + var ( + // Given this prefix... + goodprefix = []byte{'f'} + // these pairs should be found... + goodkvs = []kvSeen{ + {[]byte("foo"), []byte("bar"), false}, + {[]byte("faa"), []byte("bra"), false}, + {[]byte("foox"), []byte("barx"), false}, + } + // and these should be not. + badkvs = []kvSeen{ + {[]byte("doo"), []byte("pow"), false}, + {[]byte("mew"), []byte("qaz"), false}, + } + ) + + for _, v := range goodkvs { + require.NoError(t, s.Put(v.key, v.val)) + } + for _, v := range badkvs { + require.NoError(t, s.Put(v.key, v.val)) + } + + numFound := 0 + s.Seek(goodprefix, func(k, v []byte) { + for i := 0; i < len(goodkvs); i++ { + if string(k) == string(goodkvs[i].key) { + assert.Equal(t, string(goodkvs[i].val), string(v)) + goodkvs[i].seen = true + } + } + for i := 0; i < len(badkvs); i++ { + if string(k) == string(badkvs[i].key) { + badkvs[i].seen = true + } + } + numFound++ + }) + assert.Equal(t, len(goodkvs), numFound) + for i := 0; i < len(goodkvs); i++ { + assert.Equal(t, true, goodkvs[i].seen) + } + for i := 0; i < len(badkvs); i++ { + assert.Equal(t, false, badkvs[i].seen) + } + require.NoError(t, s.Close()) +} + +func testStoreDeleteNonExistent(t *testing.T, s Store) { + key := []byte("sparse") + + assert.NoError(t, s.Delete(key)) + require.NoError(t, s.Close()) +} + +func testStorePutAndDelete(t *testing.T, s Store) { + key := []byte("foo") + value := []byte("bar") + + require.NoError(t, s.Put(key, value)) + + err := s.Delete(key) + assert.Nil(t, err) + + _, err = s.Get(key) + assert.NotNil(t, err) + assert.Equal(t, err, ErrKeyNotFound) + + // Double delete. + err = s.Delete(key) + assert.Nil(t, err) + + require.NoError(t, s.Close()) +} + +func testStorePutBatchWithDelete(t *testing.T, s Store) { + var ( + toBeStored = map[string][]byte{ + "foo": []byte("bar"), + "bar": []byte("baz"), + } + deletedInBatch = map[string][]byte{ + "edc": []byte("rfv"), + "tgb": []byte("yhn"), + } + readdedToBatch = map[string][]byte{ + "yhn": []byte("ujm"), + } + toBeDeleted = map[string][]byte{ + "qaz": []byte("wsx"), + "qwe": []byte("123"), + } + toStay = map[string][]byte{ + "key": []byte("val"), + "faa": []byte("bra"), + } + ) + for k, v := range toBeDeleted { + require.NoError(t, s.Put([]byte(k), v)) + } + for k, v := range toStay { + require.NoError(t, s.Put([]byte(k), v)) + } + batch := s.Batch() + for k, v := range toBeStored { + batch.Put([]byte(k), v) + } + for k := range toBeDeleted { + batch.Delete([]byte(k)) + } + for k, v := range readdedToBatch { + batch.Put([]byte(k), v) + } + for k, v := range deletedInBatch { + batch.Put([]byte(k), v) + } + for k := range deletedInBatch { + batch.Delete([]byte(k)) + } + for k := range readdedToBatch { + batch.Delete([]byte(k)) + } + for k, v := range readdedToBatch { + batch.Put([]byte(k), v) + } + require.NoError(t, s.PutBatch(batch)) + toBe := []map[string][]byte{toStay, toBeStored, readdedToBatch} + notToBe := []map[string][]byte{deletedInBatch, toBeDeleted} + for _, kvs := range toBe { + for k, v := range kvs { + value, err := s.Get([]byte(k)) + assert.Nil(t, err) + assert.Equal(t, value, v) + } + } + for _, kvs := range notToBe { + for k, v := range kvs { + _, err := s.Get([]byte(k)) + assert.Equal(t, ErrKeyNotFound, err, "%s:%s", k, v) + } + } + require.NoError(t, s.Close()) +} + +func TestAllDBs(t *testing.T) { + var DBs = []dbSetup{ + {"BoltDB", newBoltStoreForTesting}, + {"LevelDB", newLevelDBForTesting}, + {"Memory", newMemoryStoreForTesting}, + {"RedisDB", newRedisStoreForTesting}, + } + var tests = []dbTestFunction{testStoreClose, testStorePutAndGet, + testStoreGetNonExistent, testStorePutBatch, testStoreSeek, + testStoreDeleteNonExistent, testStorePutAndDelete, + testStorePutBatchWithDelete} + for _, db := range DBs { + for _, test := range tests { + s := db.create(t) + twrapper := func(t *testing.T) { + test(t, s) + } + fname := runtime.FuncForPC(reflect.ValueOf(test).Pointer()).Name() + t.Run(db.name+"/"+fname, twrapper) + } + } +} From 3ada92944a976042f0170f4aa3563c356a06f100 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 7 Oct 2019 19:55:33 +0300 Subject: [PATCH 04/31] storage: drop Len from the Batch interface It's almost meaningless now and we can easily live without it. --- pkg/core/blockchain.go | 3 ++- pkg/core/storage/memory_store.go | 5 ----- pkg/core/storage/store.go | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 0365c2517..4a4afba09 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -238,6 +238,7 @@ func (bc *Blockchain) AddHeaders(headers ...*Header) (err error) { ) bc.headersOp <- func(headerList *HeaderHashList) { + oldlen := headerList.Len() for _, h := range headers { if int(h.Index-1) >= headerList.Len() { err = fmt.Errorf( @@ -258,7 +259,7 @@ func (bc *Blockchain) AddHeaders(headers ...*Header) (err error) { } } - if batch.Len() > 0 { + if oldlen != headerList.Len() { if err = bc.memStore.PutBatch(batch); err != nil { return } diff --git a/pkg/core/storage/memory_store.go b/pkg/core/storage/memory_store.go index 571ab64e5..75493914c 100644 --- a/pkg/core/storage/memory_store.go +++ b/pkg/core/storage/memory_store.go @@ -37,11 +37,6 @@ func (b *MemoryBatch) Delete(k []byte) { b.del[kcopy] = true } -// Len implements the Batch interface. -func (b *MemoryBatch) Len() int { - return len(b.m) -} - // NewMemoryStore creates a new MemoryStore object. func NewMemoryStore() *MemoryStore { return &MemoryStore{ diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index a9bbacd5b..15a25f12e 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -46,7 +46,6 @@ type ( Batch interface { Delete(k []byte) Put(k, v []byte) - Len() int } // KeyPrefix is a constant byte added as a prefix for each key From 13bf2618effbd2ef35a71204628b443d85b92750 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 7 Oct 2019 20:13:59 +0300 Subject: [PATCH 05/31] storage: improve PutBatch for MemoryStore Make it look more like a real transaction, put/delete things with a single lock. Make a copy of value in Put also, just for safety purposes, no one knows how this value slice can be used after the Put. --- pkg/core/storage/memory_store.go | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/pkg/core/storage/memory_store.go b/pkg/core/storage/memory_store.go index 75493914c..c0feaaf06 100644 --- a/pkg/core/storage/memory_store.go +++ b/pkg/core/storage/memory_store.go @@ -55,22 +55,36 @@ func (s *MemoryStore) Get(key []byte) ([]byte, error) { return nil, ErrKeyNotFound } +// put puts a key-value pair into the store, it's supposed to be called +// with mutex locked. +func (s *MemoryStore) put(key string, value []byte) { + s.mem[key] = value + delete(s.del, key) +} + // Put implements the Store interface. Never returns an error. func (s *MemoryStore) Put(key, value []byte) error { - s.mut.Lock() newKey := string(key) - s.mem[newKey] = value - delete(s.del, newKey) + vcopy := make([]byte, len(value)) + copy(vcopy, value) + s.mut.Lock() + s.put(newKey, vcopy) s.mut.Unlock() return nil } +// drop deletes a key-valu pair from the store, it's supposed to be called +// with mutex locked. +func (s *MemoryStore) drop(key string) { + s.del[key] = true + delete(s.mem, key) +} + // Delete implements Store interface. Never returns an error. func (s *MemoryStore) Delete(key []byte) error { - s.mut.Lock() newKey := string(key) - s.del[newKey] = true - delete(s.mem, newKey) + s.mut.Lock() + s.drop(newKey) s.mut.Unlock() return nil } @@ -78,11 +92,13 @@ func (s *MemoryStore) Delete(key []byte) error { // PutBatch implements the Store interface. Never returns an error. func (s *MemoryStore) PutBatch(batch Batch) error { b := batch.(*MemoryBatch) + s.mut.Lock() + defer s.mut.Unlock() for k := range b.del { - _ = s.Delete([]byte(k)) + s.drop(k) } for k, v := range b.m { - _ = s.Put([]byte(k), v) + s.put(k, v) } return nil } From 78861485b65c7764c0edbfcdd5d6c431e3b61229 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 7 Oct 2019 20:25:33 +0300 Subject: [PATCH 06/31] storage: simplify MemoryBatch It's used a lot and it looks a lot like MemoryStore, it just needs not to return errors from Put and Delete, so make it use MemoryStore internally with adjusted interface. --- pkg/core/storage/boltdb_store.go | 2 +- pkg/core/storage/memory_store.go | 21 +++++---------------- pkg/core/storage/redis_store.go | 2 +- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/pkg/core/storage/boltdb_store.go b/pkg/core/storage/boltdb_store.go index dc435a72c..e8afd0ded 100644 --- a/pkg/core/storage/boltdb_store.go +++ b/pkg/core/storage/boltdb_store.go @@ -83,7 +83,7 @@ func (s *BoltDBStore) Delete(key []byte) error { func (s *BoltDBStore) PutBatch(batch Batch) error { return s.db.Batch(func(tx *bbolt.Tx) error { b := tx.Bucket(Bucket) - for k, v := range batch.(*MemoryBatch).m { + for k, v := range batch.(*MemoryBatch).mem { err := b.Put([]byte(k), v) if err != nil { return err diff --git a/pkg/core/storage/memory_store.go b/pkg/core/storage/memory_store.go index c0feaaf06..9e74b977c 100644 --- a/pkg/core/storage/memory_store.go +++ b/pkg/core/storage/memory_store.go @@ -16,25 +16,17 @@ type MemoryStore struct { // MemoryBatch a in-memory batch compatible with MemoryStore. type MemoryBatch struct { - m map[string][]byte - // A map, not a slice, to avoid duplicates. - del map[string]bool + MemoryStore } // Put implements the Batch interface. func (b *MemoryBatch) Put(k, v []byte) { - vcopy := make([]byte, len(v)) - copy(vcopy, v) - kcopy := string(k) - b.m[kcopy] = vcopy - delete(b.del, kcopy) + _ = b.MemoryStore.Put(k, v) } // Delete implements Batch interface. func (b *MemoryBatch) Delete(k []byte) { - kcopy := string(k) - delete(b.m, kcopy) - b.del[kcopy] = true + _ = b.MemoryStore.Delete(k) } // NewMemoryStore creates a new MemoryStore object. @@ -97,7 +89,7 @@ func (s *MemoryStore) PutBatch(batch Batch) error { for k := range b.del { s.drop(k) } - for k, v := range b.m { + for k, v := range b.mem { s.put(k, v) } return nil @@ -119,10 +111,7 @@ func (s *MemoryStore) Batch() Batch { // newMemoryBatch returns new memory batch. func newMemoryBatch() *MemoryBatch { - return &MemoryBatch{ - m: make(map[string][]byte), - del: make(map[string]bool), - } + return &MemoryBatch{MemoryStore: *NewMemoryStore()} } // Persist flushes all the MemoryStore contents into the (supposedly) persistent diff --git a/pkg/core/storage/redis_store.go b/pkg/core/storage/redis_store.go index 0d8ef8e74..ae24b8c0c 100644 --- a/pkg/core/storage/redis_store.go +++ b/pkg/core/storage/redis_store.go @@ -63,7 +63,7 @@ func (s *RedisStore) Put(k, v []byte) error { // PutBatch implements the Store interface. func (s *RedisStore) PutBatch(b Batch) error { pipe := s.client.Pipeline() - for k, v := range b.(*MemoryBatch).m { + for k, v := range b.(*MemoryBatch).mem { pipe.Set(k, v, 0) } for k := range b.(*MemoryBatch).del { From 782ca64d92619e9609a5fcd5faa86d5546042959 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 8 Oct 2019 12:29:18 +0300 Subject: [PATCH 07/31] vm: accept uint16 in makeStackItem() --- pkg/vm/stack_item.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 27ec00621..f06fc0a29 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -23,6 +23,10 @@ func makeStackItem(v interface{}) StackItem { return &BigIntegerItem{ value: big.NewInt(val), } + case uint16: + return &BigIntegerItem{ + value: big.NewInt(int64(val)), + } case uint32: return &BigIntegerItem{ value: big.NewInt(int64(val)), From 6b70c5f2bd53818a7ef14c65112f7d0a26c447dc Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 16:45:30 +0300 Subject: [PATCH 08/31] keys: rename New*FromRawBytes to New*FromASN1 RawBytes is too confusing and may be read as being compatible with NEO-serialized format. --- pkg/crypto/keys/private_key.go | 4 ++-- pkg/crypto/keys/publickey.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/crypto/keys/private_key.go b/pkg/crypto/keys/private_key.go index 5ae6acad1..924f1bebe 100644 --- a/pkg/crypto/keys/private_key.go +++ b/pkg/crypto/keys/private_key.go @@ -47,8 +47,8 @@ func NewPrivateKeyFromBytes(b []byte) (*PrivateKey, error) { return &PrivateKey{b}, nil } -// NewPrivateKeyFromRawBytes returns a NEO PrivateKey from the ASN.1 serialized keys. -func NewPrivateKeyFromRawBytes(b []byte) (*PrivateKey, error) { +// NewPrivateKeyFromASN1 returns a NEO PrivateKey from the ASN.1 serialized key. +func NewPrivateKeyFromASN1(b []byte) (*PrivateKey, error) { privkey, err := x509.ParseECPrivateKey(b) if err != nil { return nil, err diff --git a/pkg/crypto/keys/publickey.go b/pkg/crypto/keys/publickey.go index bcb6cf68a..e12b9e327 100644 --- a/pkg/crypto/keys/publickey.go +++ b/pkg/crypto/keys/publickey.go @@ -78,8 +78,8 @@ func (p *PublicKey) Bytes() []byte { return append([]byte{prefix}, paddedX...) } -// NewPublicKeyFromRawBytes returns a NEO PublicKey from the ASN.1 serialized keys. -func NewPublicKeyFromRawBytes(data []byte) (*PublicKey, error) { +// NewPublicKeyFromASN1 returns a NEO PublicKey from the ASN.1 serialized key. +func NewPublicKeyFromASN1(data []byte) (*PublicKey, error) { var ( err error pubkey interface{} From acb7ef7fbd3f4a83da0d66208aefeb74ef13aff6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 16:47:27 +0300 Subject: [PATCH 09/31] vm: support uint8 and uint64 in makeStackItem() Convenience to avoid casts, uint64 is also a bit special in that it can't be converted to int64 without data loss. --- pkg/vm/stack_item.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index f06fc0a29..468453d7b 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -1,6 +1,7 @@ package vm import ( + "encoding/binary" "encoding/json" "fmt" "math/big" @@ -23,6 +24,10 @@ func makeStackItem(v interface{}) StackItem { return &BigIntegerItem{ value: big.NewInt(val), } + case uint8: + return &BigIntegerItem{ + value: big.NewInt(int64(val)), + } case uint16: return &BigIntegerItem{ value: big.NewInt(int64(val)), @@ -31,6 +36,14 @@ func makeStackItem(v interface{}) StackItem { return &BigIntegerItem{ value: big.NewInt(int64(val)), } + case uint64: + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, val) + bigInt := big.NewInt(0) + bigInt.SetBytes(b) + return &BigIntegerItem{ + value: bigInt, + } case []byte: return &ByteArrayItem{ value: val, From 238c590ddb798a5a6940c6a07712f63db9fd30f9 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 17:56:58 +0300 Subject: [PATCH 10/31] core: fix contract state's Properties to use PropertyState PublishTX only had one of these flags, but newer contracts (created via the interop function) can have more and these flags are aggregated into one field that uses PropertyState enumeration (it's used to publish contract, so supposedly it's also a nice choice for contract state storage). --- pkg/core/blockchain.go | 7 ++++- pkg/core/contract_state.go | 43 ++++++++++++++++++------------ pkg/core/contract_state_test.go | 35 ++++++++++++++++-------- pkg/smartcontract/param_context.go | 11 ++++++++ 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 4a4afba09..57da76ff2 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -13,6 +13,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm" "github.com/pkg/errors" @@ -390,11 +391,15 @@ func (bc *Blockchain) storeBlock(block *Block) error { case *transaction.EnrollmentTX: case *transaction.StateTX: case *transaction.PublishTX: + var properties smartcontract.PropertyState + if t.NeedStorage { + properties |= smartcontract.HasStorage + } contract := &ContractState{ Script: t.Script, ParamList: t.ParamList, ReturnType: t.ReturnType, - HasStorage: t.NeedStorage, + Properties: properties, Name: t.Name, CodeVersion: t.CodeVersion, Author: t.Author, diff --git a/pkg/core/contract_state.go b/pkg/core/contract_state.go index 1ba19cbaa..157e7dd60 100644 --- a/pkg/core/contract_state.go +++ b/pkg/core/contract_state.go @@ -13,17 +13,15 @@ type Contracts map[util.Uint160]*ContractState // ContractState holds information about a smart contract in the NEO blockchain. type ContractState struct { - Script []byte - ParamList []smartcontract.ParamType - ReturnType smartcontract.ParamType - Properties []byte - Name string - CodeVersion string - Author string - Email string - Description string - HasStorage bool - HasDynamicInvoke bool + Script []byte + ParamList []smartcontract.ParamType + ReturnType smartcontract.ParamType + Properties smartcontract.PropertyState + Name string + CodeVersion string + Author string + Email string + Description string scriptHash util.Uint160 } @@ -52,14 +50,12 @@ func (a *ContractState) DecodeBinary(br *io.BinReader) { a.ParamList[k] = smartcontract.ParamType(paramBytes[k]) } br.ReadLE(&a.ReturnType) - a.Properties = br.ReadBytes() + br.ReadLE(&a.Properties) a.Name = br.ReadString() a.CodeVersion = br.ReadString() a.Author = br.ReadString() a.Email = br.ReadString() a.Description = br.ReadString() - br.ReadLE(&a.HasStorage) - br.ReadLE(&a.HasDynamicInvoke) a.createHash() } @@ -71,14 +67,12 @@ func (a *ContractState) EncodeBinary(bw *io.BinWriter) { bw.WriteLE(a.ParamList[k]) } bw.WriteLE(a.ReturnType) - bw.WriteBytes(a.Properties) + bw.WriteLE(a.Properties) bw.WriteString(a.Name) bw.WriteString(a.CodeVersion) bw.WriteString(a.Author) bw.WriteString(a.Email) bw.WriteString(a.Description) - bw.WriteLE(a.HasStorage) - bw.WriteLE(a.HasDynamicInvoke) } // ScriptHash returns a contract script hash. @@ -93,3 +87,18 @@ func (a *ContractState) ScriptHash() util.Uint160 { func (a *ContractState) createHash() { a.scriptHash = hash.Hash160(a.Script) } + +// HasStorage checks whether the contract has storage property set. +func (cs *ContractState) HasStorage() bool { + return (cs.Properties & smartcontract.HasStorage) != 0 +} + +// HasDynamicInvoke checks whether the contract has dynamic invoke property set. +func (cs *ContractState) HasDynamicInvoke() bool { + return (cs.Properties & smartcontract.HasDynamicInvoke) != 0 +} + +// IsPayable checks whether the contract has payable property set. +func (cs *ContractState) IsPayable() bool { + return (cs.Properties & smartcontract.IsPayable) != 0 +} diff --git a/pkg/core/contract_state_test.go b/pkg/core/contract_state_test.go index a34039aae..276e34e5f 100644 --- a/pkg/core/contract_state_test.go +++ b/pkg/core/contract_state_test.go @@ -13,17 +13,15 @@ func TestEncodeDecodeContractState(t *testing.T) { script := []byte("testscript") contract := &ContractState{ - Script: script, - ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type}, - ReturnType: smartcontract.BoolType, - Properties: []byte("smth"), - Name: "Contracto", - CodeVersion: "1.0.0", - Author: "Joe Random", - Email: "joe@example.com", - Description: "Test contract", - HasStorage: true, - HasDynamicInvoke: false, + Script: script, + ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type}, + ReturnType: smartcontract.BoolType, + Properties: smartcontract.HasStorage, + Name: "Contracto", + CodeVersion: "1.0.0", + Author: "Joe Random", + Email: "joe@example.com", + Description: "Test contract", } assert.Equal(t, hash.Hash160(script), contract.ScriptHash()) @@ -37,3 +35,18 @@ func TestEncodeDecodeContractState(t *testing.T) { assert.Equal(t, contract, contractDecoded) assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash()) } + +func TestContractStateProperties(t *testing.T) { + flaggedContract := ContractState{ + Properties: smartcontract.HasStorage | smartcontract.HasDynamicInvoke | smartcontract.IsPayable, + } + nonFlaggedContract := ContractState{ + ReturnType: smartcontract.BoolType, + } + assert.Equal(t, true, flaggedContract.HasStorage()) + assert.Equal(t, true, flaggedContract.HasDynamicInvoke()) + assert.Equal(t, true, flaggedContract.IsPayable()) + assert.Equal(t, false, nonFlaggedContract.HasStorage()) + assert.Equal(t, false, nonFlaggedContract.HasDynamicInvoke()) + assert.Equal(t, false, nonFlaggedContract.IsPayable()) +} diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index b4b75c45c..e5ad6142b 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -18,6 +18,17 @@ const ( ArrayType ) +// PropertyState represents contract properties (flags). +type PropertyState byte + +// List of supported properties. +const ( + NoProperties = 0 + HasStorage PropertyState = 1 << iota + HasDynamicInvoke + IsPayable +) + // Parameter represents a smart contract parameter. type Parameter struct { // Type of the parameter From c5a4cfaebeb32ce10e5e67cdf189c3c8e7103e89 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 18:22:06 +0300 Subject: [PATCH 11/31] smartcontract: add CreateSignatureRedeemScript() It's very similar to the CreateMultiSigRedeemScript() and it will be used in interop functions. --- pkg/smartcontract/contract.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/smartcontract/contract.go b/pkg/smartcontract/contract.go index 7e978bfd3..095f9f8e9 100644 --- a/pkg/smartcontract/contract.go +++ b/pkg/smartcontract/contract.go @@ -9,6 +9,21 @@ import ( "github.com/CityOfZion/neo-go/pkg/vm" ) +// CreateSignatureRedeemScript creates a check signature script runnable by VM. +func CreateSignatureRedeemScript(key *keys.PublicKey) ([]byte, error) { + buf := new(bytes.Buffer) + err := vm.EmitBytes(buf, key.Bytes()) + if err != nil { + return nil, err + } + err = vm.EmitOpcode(buf, vm.CHECKSIG) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + // CreateMultiSigRedeemScript will create a script runnable by the VM. func CreateMultiSigRedeemScript(m int, publicKeys keys.PublicKeys) ([]byte, error) { if m <= 1 { From 7ab58ff8cbd1023e6fc25a1a1d03f7d98e75194b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 18:40:16 +0300 Subject: [PATCH 12/31] keys: make public key's IsInfinity() public It's gonna be used in interops for key validity check. --- pkg/crypto/keys/publickey.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/crypto/keys/publickey.go b/pkg/crypto/keys/publickey.go index e12b9e327..b8f039de9 100644 --- a/pkg/crypto/keys/publickey.go +++ b/pkg/crypto/keys/publickey.go @@ -61,7 +61,7 @@ func NewPublicKeyFromString(s string) (*PublicKey, error) { // Bytes returns the byte array representation of the public key. func (p *PublicKey) Bytes() []byte { - if p.isInfinity() { + if p.IsInfinity() { return []byte{0x00} } @@ -225,14 +225,14 @@ func (p *PublicKey) Verify(signature []byte, hash []byte) bool { return ecdsa.Verify(publicKey, hash, rBytes, sBytes) } -// isInfinity checks if point P is infinity on EllipticCurve ec. -func (p *PublicKey) isInfinity() bool { +// IsInfinity checks if the key is infinite (null, basically). +func (p *PublicKey) IsInfinity() bool { return p.X == nil && p.Y == nil } // String implements the Stringer interface. func (p *PublicKey) String() string { - if p.isInfinity() { + if p.IsInfinity() { return "00" } bx := hex.EncodeToString(p.X.Bytes()) From d007cc00cc4357cc18d12b0e3cca72cd1d234066 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 19:52:10 +0300 Subject: [PATCH 13/31] vm: add script check functions These are required for future interops. --- pkg/vm/contract_checks.go | 98 ++++++++++++++ pkg/vm/contract_checks_test.go | 227 +++++++++++++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 pkg/vm/contract_checks.go create mode 100644 pkg/vm/contract_checks_test.go diff --git a/pkg/vm/contract_checks.go b/pkg/vm/contract_checks.go new file mode 100644 index 000000000..f51e7c99b --- /dev/null +++ b/pkg/vm/contract_checks.go @@ -0,0 +1,98 @@ +package vm + +import ( + "encoding/binary" +) + +func getNumOfThingsFromInstr(instr Instruction, param []byte) (int, bool) { + var nthings int + + switch instr { + case PUSH1, PUSH2, PUSH3, PUSH4, PUSH5, PUSH6, PUSH7, PUSH8, + PUSH9, PUSH10, PUSH11, PUSH12, PUSH13, PUSH14, PUSH15, PUSH16: + nthings = int(instr-PUSH1) + 1 + case PUSHBYTES1: + nthings = int(param[0]) + case PUSHBYTES2: + nthings = int(binary.LittleEndian.Uint16(param)) + default: + return 0, false + } + if nthings < 1 || nthings > MaxArraySize { + return 0, false + } + return nthings, true +} + +// IsMultiSigContract checks whether the passed script is a multi-signature +// contract. +func IsMultiSigContract(script []byte) bool { + var nsigs, nkeys int + + ctx := NewContext(script) + instr, param, err := ctx.Next() + if err != nil { + return false + } + nsigs, ok := getNumOfThingsFromInstr(instr, param) + if !ok { + return false + } + for { + instr, param, err = ctx.Next() + if err != nil { + return false + } + if instr != PUSHBYTES33 { + break + } + nkeys++ + if nkeys > MaxArraySize { + return false + } + } + if nkeys < nsigs { + return false + } + nkeys2, ok := getNumOfThingsFromInstr(instr, param) + if !ok { + return false + } + if nkeys2 != nkeys { + return false + } + instr, _, err = ctx.Next() + if err != nil || instr != CHECKMULTISIG { + return false + } + instr, _, err = ctx.Next() + if err != nil || instr != RET || ctx.ip != len(script) { + return false + } + return true +} + +// IsSignatureContract checks whether the passed script is a signature check +// contract. +func IsSignatureContract(script []byte) bool { + ctx := NewContext(script) + instr, _, err := ctx.Next() + if err != nil || instr != PUSHBYTES33 { + return false + } + instr, _, err = ctx.Next() + if err != nil || instr != CHECKSIG { + return false + } + instr, _, err = ctx.Next() + if err != nil || instr != RET || ctx.ip != len(script) { + return false + } + return true +} + +// IsStandardContract checks whether the passed script is a signature or +// multi-signature contract. +func IsStandardContract(script []byte) bool { + return IsSignatureContract(script) || IsMultiSigContract(script) +} diff --git a/pkg/vm/contract_checks_test.go b/pkg/vm/contract_checks_test.go new file mode 100644 index 000000000..791912822 --- /dev/null +++ b/pkg/vm/contract_checks_test.go @@ -0,0 +1,227 @@ +package vm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsSignatureContractGood(t *testing.T) { + prog := make([]byte, 35) + prog[0] = byte(PUSHBYTES33) + prog[34] = byte(CHECKSIG) + assert.Equal(t, true, IsSignatureContract(prog)) + assert.Equal(t, true, IsStandardContract(prog)) +} + +func TestIsSignatureContractBadNoCheckSig(t *testing.T) { + prog := make([]byte, 34) + prog[0] = byte(PUSHBYTES33) + assert.Equal(t, false, IsSignatureContract(prog)) + assert.Equal(t, false, IsStandardContract(prog)) +} + +func TestIsSignatureContractBadNoCheckSig2(t *testing.T) { + prog := make([]byte, 35) + prog[0] = byte(PUSHBYTES33) + prog[34] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsSignatureContract(prog)) +} + +func TestIsSignatureContractBadWrongPush(t *testing.T) { + prog := make([]byte, 35) + prog[0] = byte(PUSHBYTES32) + prog[33] = byte(NOP) + prog[34] = byte(CHECKSIG) + assert.Equal(t, false, IsSignatureContract(prog)) +} + +func TestIsSignatureContractBadWrongInstr(t *testing.T) { + prog := make([]byte, 30) + prog[0] = byte(PUSHBYTES33) + assert.Equal(t, false, IsSignatureContract(prog)) +} + +func TestIsSignatureContractBadExcessiveInstr(t *testing.T) { + prog := make([]byte, 36) + prog[0] = byte(PUSHBYTES33) + prog[34] = byte(CHECKSIG) + prog[35] = byte(RET) + assert.Equal(t, false, IsSignatureContract(prog)) +} + +func TestIsMultiSigContractGood(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(CHECKMULTISIG) + assert.Equal(t, true, IsMultiSigContract(prog)) + assert.Equal(t, true, IsStandardContract(prog)) +} + +func TestIsMultiSigContractGoodPushBytes1(t *testing.T) { + prog := make([]byte, 73) + prog[0] = byte(PUSHBYTES1) + prog[1] = 2 + prog[2] = byte(PUSHBYTES33) + prog[36] = byte(PUSHBYTES33) + prog[70] = byte(PUSHBYTES1) + prog[71] = 2 + prog[72] = byte(CHECKMULTISIG) + assert.Equal(t, true, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractGoodPushBytes2(t *testing.T) { + prog := make([]byte, 75) + prog[0] = byte(PUSHBYTES2) + prog[1] = 2 + prog[3] = byte(PUSHBYTES33) + prog[37] = byte(PUSHBYTES33) + prog[71] = byte(PUSHBYTES2) + prog[72] = 2 + prog[74] = byte(CHECKMULTISIG) + assert.Equal(t, true, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadNSigs1(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSH0) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) + assert.Equal(t, false, IsStandardContract(prog)) +} + +func TestIsMultiSigContractBadNSigs2(t *testing.T) { + prog := make([]byte, 73) + prog[0] = byte(PUSHBYTES2) + prog[1] = 0xff + prog[2] = 0xff + prog[3] = byte(PUSHBYTES33) + prog[37] = byte(PUSHBYTES33) + prog[71] = byte(PUSH2) + prog[72] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadNSigs3(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSH5) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadExcessiveNOP1(t *testing.T) { + prog := make([]byte, 72) + prog[0] = byte(PUSH2) + prog[1] = byte(NOP) + prog[2] = byte(PUSHBYTES33) + prog[36] = byte(PUSHBYTES33) + prog[70] = byte(PUSH2) + prog[71] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadExcessiveNOP2(t *testing.T) { + prog := make([]byte, 72) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(NOP) + prog[36] = byte(PUSHBYTES33) + prog[70] = byte(PUSH2) + prog[71] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadExcessiveNOP3(t *testing.T) { + prog := make([]byte, 72) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(NOP) + prog[70] = byte(PUSH2) + prog[71] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadExcessiveNOP4(t *testing.T) { + prog := make([]byte, 72) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(NOP) + prog[71] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadExcessiveNOP5(t *testing.T) { + prog := make([]byte, 72) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(CHECKMULTISIG) + prog[71] = byte(NOP) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadNKeys1(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH3) + prog[70] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadNKeys2(t *testing.T) { + prog := make([]byte, 1) + prog[0] = byte(PUSH10) + key := make([]byte, 33) + var asize = uint16(MaxArraySize + 1) + for i := 0; i < int(asize); i++ { + prog = append(prog, byte(PUSHBYTES33)) + prog = append(prog, key...) + } + prog = append(prog, byte(PUSHBYTES2), byte(asize&0xff), byte((asize<<8)&0xff), byte(CHECKMULTISIG)) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadRead1(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSHBYTES75) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadRead2(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES75) + prog[69] = byte(PUSH2) + prog[70] = byte(CHECKMULTISIG) + assert.Equal(t, false, IsMultiSigContract(prog)) +} + +func TestIsMultiSigContractBadRead3(t *testing.T) { + prog := make([]byte, 71) + prog[0] = byte(PUSH2) + prog[1] = byte(PUSHBYTES33) + prog[35] = byte(PUSHBYTES33) + prog[69] = byte(PUSH2) + prog[70] = byte(PUSHBYTES1) + assert.Equal(t, false, IsMultiSigContract(prog)) +} From 258f397b9a0750dc9342e4e7e98b913859358d7c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 10 Oct 2019 20:02:09 +0300 Subject: [PATCH 14/31] core: append transactions to the block in GetBlock() We want to get a full block, so it has to have transactions inside. Unfortunately our tests were used to this wrong behavior and utilized completely bogus transactions without data that couldn't be persisted, so fix that also. --- pkg/core/block_test.go | 4 ++-- pkg/core/blockchain.go | 7 +++++++ pkg/core/blockchain_test.go | 9 ++++----- pkg/core/helper_test.go | 14 +++++++++++--- pkg/rpc/server_helper_test.go | 7 ++++--- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/pkg/core/block_test.go b/pkg/core/block_test.go index 164a0ab88..b8e390c71 100644 --- a/pkg/core/block_test.go +++ b/pkg/core/block_test.go @@ -84,8 +84,8 @@ func TestHashBlockEqualsHashHeader(t *testing.T) { func TestBlockVerify(t *testing.T) { block := newBlock( 0, - newTX(transaction.MinerType), - newTX(transaction.IssueType), + newMinerTX(), + newIssueTX(), ) assert.True(t, block.Verify(false)) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 57da76ff2..7013e3719 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -523,6 +523,13 @@ func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) { if len(block.Transactions) == 0 { return nil, fmt.Errorf("only header is available") } + for _, tx := range block.Transactions { + stx, _, err := bc.GetTransaction(tx.Hash()) + if err != nil { + return nil, err + } + *tx = *stx + } return block, nil } diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 8dab93802..9938ac34a 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -6,7 +6,6 @@ import ( "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/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,9 +38,9 @@ func TestAddHeaders(t *testing.T) { func TestAddBlock(t *testing.T) { bc := newTestChain(t) blocks := []*Block{ - newBlock(1, newTX(transaction.MinerType)), - newBlock(2, newTX(transaction.MinerType)), - newBlock(3, newTX(transaction.MinerType)), + newBlock(1, newMinerTX()), + newBlock(2, newMinerTX()), + newBlock(3, newMinerTX()), } for i := 0; i < len(blocks); i++ { @@ -70,7 +69,7 @@ func TestAddBlock(t *testing.T) { func TestGetHeader(t *testing.T) { bc := newTestChain(t) - block := newBlock(1, newTX(transaction.MinerType)) + block := newBlock(1, newMinerTX()) err := bc.AddBlock(block) assert.Nil(t, err) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 65c44a8c0..205ae8bbc 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -40,14 +40,22 @@ func newBlock(index uint32, txs ...*transaction.Transaction) *Block { func makeBlocks(n int) []*Block { blocks := make([]*Block, n) for i := 0; i < n; i++ { - blocks[i] = newBlock(uint32(i+1), newTX(transaction.MinerType)) + blocks[i] = newBlock(uint32(i+1), newMinerTX()) } return blocks } -func newTX(t transaction.TXType) *transaction.Transaction { +func newMinerTX() *transaction.Transaction { return &transaction.Transaction{ - Type: t, + Type: transaction.MinerType, + Data: &transaction.MinerTX{}, + } +} + +func newIssueTX() *transaction.Transaction { + return &transaction.Transaction{ + Type: transaction.IssueType, + Data: &transaction.IssueTX{}, } } diff --git a/pkg/rpc/server_helper_test.go b/pkg/rpc/server_helper_test.go index fd818750f..bbb7f8b34 100644 --- a/pkg/rpc/server_helper_test.go +++ b/pkg/rpc/server_helper_test.go @@ -172,14 +172,15 @@ func initBlocks(t *testing.T, chain *core.Blockchain) { func makeBlocks(n int) []*core.Block { blocks := make([]*core.Block, n) for i := 0; i < n; i++ { - blocks[i] = newBlock(uint32(i+1), newTX(transaction.MinerType)) + blocks[i] = newBlock(uint32(i+1), newMinerTX()) } return blocks } -func newTX(t transaction.TXType) *transaction.Transaction { +func newMinerTX() *transaction.Transaction { return &transaction.Transaction{ - Type: t, + Type: transaction.MinerType, + Data: &transaction.MinerTX{}, } } From 16bc5296cbdf60f5a3debc494e0b989998064bcc Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 11:40:54 +0300 Subject: [PATCH 15/31] block: return error from Verify Don't hide real problem behind the bool value. Makes it easier to identify problems when looking at log messages. --- pkg/core/block.go | 13 ++++++++----- pkg/core/block_test.go | 6 +++--- pkg/core/blockchain.go | 5 +++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/core/block.go b/pkg/core/block.go index fddb06eba..23bc73c40 100644 --- a/pkg/core/block.go +++ b/pkg/core/block.go @@ -1,6 +1,9 @@ package core import ( + "errors" + "fmt" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/io" @@ -45,26 +48,26 @@ func (b *Block) rebuildMerkleRoot() error { } // Verify the integrity of the block. -func (b *Block) Verify(full bool) bool { +func (b *Block) Verify(full bool) error { // There has to be some transaction inside. if len(b.Transactions) == 0 { - return false + return errors.New("no transactions") } // The first TX has to be a miner transaction. if b.Transactions[0].Type != transaction.MinerType { - return false + return fmt.Errorf("the first transaction is %s", b.Transactions[0].Type) } // If the first TX is a minerTX then all others cant. for _, tx := range b.Transactions[1:] { if tx.Type == transaction.MinerType { - return false + return fmt.Errorf("miner transaction %s is not the first one", tx.Hash().ReverseString()) } } // TODO: When full is true, do a full verification. if full { log.Warn("full verification of blocks is not yet implemented") } - return true + return nil } // NewBlockFromTrimmedBytes returns a new block from trimmed data. diff --git a/pkg/core/block_test.go b/pkg/core/block_test.go index b8e390c71..bb7016826 100644 --- a/pkg/core/block_test.go +++ b/pkg/core/block_test.go @@ -87,19 +87,19 @@ func TestBlockVerify(t *testing.T) { newMinerTX(), newIssueTX(), ) - assert.True(t, block.Verify(false)) + assert.Nil(t, block.Verify(false)) block.Transactions = []*transaction.Transaction{ {Type: transaction.IssueType}, {Type: transaction.MinerType}, } - assert.False(t, block.Verify(false)) + assert.NotNil(t, block.Verify(false)) block.Transactions = []*transaction.Transaction{ {Type: transaction.MinerType}, {Type: transaction.MinerType}, } - assert.False(t, block.Verify(false)) + assert.NotNil(t, block.Verify(false)) } func TestBinBlockDecodeEncode(t *testing.T) { diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 7013e3719..bf5abadca 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -210,8 +210,9 @@ func (bc *Blockchain) AddBlock(block *Block) error { return fmt.Errorf("expected block %d, but passed block %d", expectedHeight, block.Index) } if bc.verifyBlocks { - if !block.Verify(false) { - return fmt.Errorf("block %s is invalid", block.Hash()) + err := block.Verify(false) + if err != nil { + return fmt.Errorf("block %s is invalid: %s", block.Hash().ReverseString(), err) } for _, tx := range block.Transactions { err := bc.Verify(tx) From cffe8d0ee2de9dc601b8d1310360ca444db2f6f7 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 11:42:36 +0300 Subject: [PATCH 16/31] core: return error from verifyResults() Make it easier to find out what the real problem is. --- pkg/core/blockchain.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index bf5abadca..002f53e37 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -788,8 +788,8 @@ func (bc *Blockchain) Verify(t *transaction.Transaction) error { if ok := bc.verifyOutputs(t); !ok { return errors.New("invalid transaction's outputs") } - if ok := bc.verifyResults(t); !ok { - return errors.New("invalid transaction's results") + if err := bc.verifyResults(t); err != nil { + return err } for _, a := range t.Attributes { @@ -834,10 +834,10 @@ func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) bool { return true } -func (bc *Blockchain) verifyResults(t *transaction.Transaction) bool { +func (bc *Blockchain) verifyResults(t *transaction.Transaction) error { results := bc.GetTransationResults(t) if results == nil { - return false + return errors.New("tx has no results") } var resultsDestroy []*transaction.Result var resultsIssue []*transaction.Result @@ -851,38 +851,44 @@ func (bc *Blockchain) verifyResults(t *transaction.Transaction) bool { } } if len(resultsDestroy) > 1 { - return false + return errors.New("tx has more than 1 destroy output") } if len(resultsDestroy) == 1 && resultsDestroy[0].AssetID != utilityTokenTX().Hash() { - return false + return errors.New("tx destroys non-utility token") } - if bc.SystemFee(t).GreaterThan(util.Fixed8(0)) && (len(resultsDestroy) == 0 || resultsDestroy[0].Amount.LessThan(bc.SystemFee(t))) { - return false + sysfee := bc.SystemFee(t) + if sysfee.GreaterThan(util.Fixed8(0)) { + if len(resultsDestroy) == 0 { + return fmt.Errorf("system requires to pay %s fee, but tx pays nothing", sysfee.String()) + } + if resultsDestroy[0].Amount.LessThan(sysfee) { + return fmt.Errorf("system requires to pay %s fee, but tx pays %s only", sysfee.String(), resultsDestroy[0].Amount.String()) + } } switch t.Type { case transaction.MinerType, transaction.ClaimType: for _, r := range resultsIssue { if r.AssetID != utilityTokenTX().Hash() { - return false + return errors.New("miner or claim tx issues non-utility tokens") } } break case transaction.IssueType: for _, r := range resultsIssue { if r.AssetID == utilityTokenTX().Hash() { - return false + return errors.New("issue tx issues utility tokens") } } break default: if len(resultsIssue) > 0 { - return false + return errors.New("non issue/miner/claim tx issues tokens") } break } - return true + return nil } // GetTransationResults returns the transaction results aggregate by assetID. From ab4ff79e0243932f7dd97da71359cb8287ac5477 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 11:57:48 +0300 Subject: [PATCH 17/31] core: fix typo in GetTransactionResults() --- pkg/core/blockchain.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 002f53e37..448f299c8 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -835,7 +835,7 @@ func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) bool { } func (bc *Blockchain) verifyResults(t *transaction.Transaction) error { - results := bc.GetTransationResults(t) + results := bc.GetTransactionResults(t) if results == nil { return errors.New("tx has no results") } @@ -891,9 +891,9 @@ func (bc *Blockchain) verifyResults(t *transaction.Transaction) error { return nil } -// GetTransationResults returns the transaction results aggregate by assetID. +// GetTransactionResults returns the transaction results aggregate by assetID. // Golang of GetTransationResults method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L207) -func (bc *Blockchain) GetTransationResults(t *transaction.Transaction) []*transaction.Result { +func (bc *Blockchain) GetTransactionResults(t *transaction.Transaction) []*transaction.Result { var tempResults []*transaction.Result var results []*transaction.Result tempGroupResult := make(map[util.Uint256]util.Fixed8) From 96934cfeab8622564af652cbd9eb2ef88caaf3b3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 12:09:16 +0300 Subject: [PATCH 18/31] core: return error from verifyOutputs() Make debugging easier. --- pkg/core/blockchain.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 448f299c8..5559286b6 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -785,8 +785,8 @@ func (bc *Blockchain) Verify(t *transaction.Transaction) error { if IsDoubleSpend(bc.Store, t) { return errors.New("invalid transaction caused by double spending") } - if ok := bc.verifyOutputs(t); !ok { - return errors.New("invalid transaction's outputs") + if err := bc.verifyOutputs(t); err != nil { + return errors.Wrap(err, "wrong outputs") } if err := bc.verifyResults(t); err != nil { return err @@ -813,25 +813,25 @@ func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool { return true } -func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) bool { +func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) error { for assetID, outputs := range t.GroupOutputByAssetID() { assetState := bc.GetAssetState(assetID) if assetState == nil { - return false + return fmt.Errorf("no asset state for %s", assetID.ReverseString()) } if assetState.Expiration < bc.blockHeight+1 && assetState.AssetType != transaction.GoverningToken && assetState.AssetType != transaction.UtilityToken { - return false + return fmt.Errorf("asset %s expired", assetID.ReverseString()) } for _, out := range outputs { if int64(out.Amount)%int64(math.Pow10(8-int(assetState.Precision))) != 0 { - return false + return fmt.Errorf("output is not compliant with %s asset precision", assetID.ReverseString()) } } } - return true + return nil } func (bc *Blockchain) verifyResults(t *transaction.Transaction) error { From 35824728eb1a9c070af37b4561a0295ff3894695 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 12:15:16 +0300 Subject: [PATCH 19/31] core: add putAssetStateIntoStore() Will be used by interops. --- pkg/core/asset_state.go | 11 +++++++++++ pkg/core/asset_state_test.go | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pkg/core/asset_state.go b/pkg/core/asset_state.go index 3a553d7ef..8dd8663e6 100644 --- a/pkg/core/asset_state.go +++ b/pkg/core/asset_state.go @@ -27,6 +27,17 @@ func (a Assets) commit(b storage.Batch) error { return nil } +// putAssetStateIntoStore puts given asset state into the given store. +func putAssetStateIntoStore(s storage.Store, as *AssetState) error { + buf := io.NewBufBinWriter() + as.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + key := storage.AppendPrefix(storage.STAsset, as.ID.Bytes()) + return s.Put(key, buf.Bytes()) +} + // AssetState represents the state of an NEO registered Asset. type AssetState struct { ID util.Uint256 diff --git a/pkg/core/asset_state_test.go b/pkg/core/asset_state_test.go index 203897c47..8fbfb3791 100644 --- a/pkg/core/asset_state_test.go +++ b/pkg/core/asset_state_test.go @@ -3,6 +3,7 @@ package core import ( "testing" + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/io" @@ -35,3 +36,25 @@ func TestEncodeDecodeAssetState(t *testing.T) { assert.Nil(t, r.Err) assert.Equal(t, asset, assetDecode) } + +func TestPutGetAssetState(t *testing.T) { + s := storage.NewMemoryStore() + asset := &AssetState{ + ID: randomUint256(), + AssetType: transaction.Token, + Name: "super cool token", + Amount: util.Fixed8(1000000), + Available: util.Fixed8(100), + Precision: 8, + FeeMode: feeMode, + Owner: &keys.PublicKey{}, + Admin: randomUint160(), + Issuer: randomUint160(), + Expiration: 10, + IsFrozen: false, + } + assert.NoError(t, putAssetStateIntoStore(s, asset)) + asRead := getAssetStateFromStore(s, asset.ID) + assert.NotNil(t, asRead) + assert.Equal(t, asset, asRead) +} From 58d9b79fe33fe4c51f1c36f5c5890fead2c5af3a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 12:21:09 +0300 Subject: [PATCH 20/31] core: avoid duplicates in GetScriptHashesForVerifyingClaim() As they can break VerifyWitnesses(). --- pkg/core/blockchain.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 5559286b6..8e30350e9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -938,7 +938,8 @@ func (bc *Blockchain) GetTransactionResults(t *transaction.Transaction) []*trans // GetScriptHashesForVerifyingClaim returns all ScriptHashes of Claim transaction // which has a different implementation from generic GetScriptHashesForVerifying. func (bc *Blockchain) GetScriptHashesForVerifyingClaim(t *transaction.Transaction) ([]util.Uint160, error) { - hashes := make([]util.Uint160, 0) + // Avoiding duplicates. + hashmap := make(map[util.Uint160]bool) claim := t.Data.(*transaction.ClaimTX) clGroups := make(map[util.Uint256][]*transaction.Input) @@ -954,10 +955,14 @@ func (bc *Blockchain) GetScriptHashesForVerifyingClaim(t *transaction.Transactio if len(refTx.Outputs) <= int(input.PrevIndex) { return nil, fmt.Errorf("wrong PrevIndex reference") } - hashes = append(hashes, refTx.Outputs[input.PrevIndex].ScriptHash) + hashmap[refTx.Outputs[input.PrevIndex].ScriptHash] = true } } - if len(hashes) > 0 { + if len(hashmap) > 0 { + hashes := make([]util.Uint160, 0, len(hashmap)) + for k := range hashmap { + hashes = append(hashes, k) + } return hashes, nil } return nil, fmt.Errorf("no hashes found") From 2156d5db738a9f6931db75404f8bc00f1e5f6b97 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 13:11:54 +0300 Subject: [PATCH 21/31] core: add put/delete functions for ContractState These are gonna be used by interops. --- pkg/core/contract_state.go | 17 +++++++++++++++++ pkg/core/contract_state_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/pkg/core/contract_state.go b/pkg/core/contract_state.go index 157e7dd60..cd3c8aea9 100644 --- a/pkg/core/contract_state.go +++ b/pkg/core/contract_state.go @@ -75,6 +75,23 @@ func (a *ContractState) EncodeBinary(bw *io.BinWriter) { bw.WriteString(a.Description) } +// putContractStateIntoStore puts given contract state into the given store. +func putContractStateIntoStore(s storage.Store, cs *ContractState) error { + buf := io.NewBufBinWriter() + cs.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + key := storage.AppendPrefix(storage.STContract, cs.ScriptHash().Bytes()) + return s.Put(key, buf.Bytes()) +} + +// deleteContractStateInStore deletes given contract state in the given store. +func deleteContractStateInStore(s storage.Store, hash util.Uint160) error { + key := storage.AppendPrefix(storage.STContract, hash.Bytes()) + return s.Delete(key) +} + // ScriptHash returns a contract script hash. func (a *ContractState) ScriptHash() util.Uint160 { if a.scriptHash.Equals(util.Uint160{}) { diff --git a/pkg/core/contract_state_test.go b/pkg/core/contract_state_test.go index 276e34e5f..1507ec566 100644 --- a/pkg/core/contract_state_test.go +++ b/pkg/core/contract_state_test.go @@ -3,6 +3,7 @@ package core import ( "testing" + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/smartcontract" @@ -50,3 +51,27 @@ func TestContractStateProperties(t *testing.T) { assert.Equal(t, false, nonFlaggedContract.HasDynamicInvoke()) assert.Equal(t, false, nonFlaggedContract.IsPayable()) } + +func TestPutGetDeleteContractState(t *testing.T) { + s := storage.NewMemoryStore() + script := []byte("testscript") + + contract := &ContractState{ + Script: script, + ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type}, + ReturnType: smartcontract.BoolType, + Properties: smartcontract.HasStorage, + Name: "Contracto", + CodeVersion: "1.0.0", + Author: "Joe Random", + Email: "joe@example.com", + Description: "Test contract", + } + assert.NoError(t, putContractStateIntoStore(s, contract)) + csRead := getContractStateFromStore(s, contract.ScriptHash()) + assert.NotNil(t, csRead) + assert.Equal(t, contract, csRead) + assert.NoError(t, deleteContractStateInStore(s, contract.ScriptHash())) + csRead2 := getContractStateFromStore(s, contract.ScriptHash()) + assert.Nil(t, csRead2) +} From bfddf9b3f66043c4f2e4a49591201670c9f043f5 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 14:08:21 +0300 Subject: [PATCH 22/31] core: implement StorageItem for future interops --- pkg/core/blockchain.go | 37 ++++++++++++++++++++ pkg/core/blockchainer.go | 2 ++ pkg/core/storage_item.go | 64 +++++++++++++++++++++++++++++++++++ pkg/core/storage_item_test.go | 26 ++++++++++++++ pkg/network/helper_test.go | 6 ++++ 5 files changed, 135 insertions(+) create mode 100644 pkg/core/storage_item.go create mode 100644 pkg/core/storage_item_test.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 8e30350e9..d8051e9c0 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -512,6 +512,43 @@ func getTransactionFromStore(s storage.Store, hash util.Uint256) (*transaction.T return tx, height, nil } +// GetStorageItem returns an item from storage. +func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem { + sItem := getStorageItemFromStore(bc.memStore, scripthash, key) + if sItem == nil { + sItem = getStorageItemFromStore(bc.Store, scripthash, key) + } + return sItem +} + +// GetStorageItems returns all storage items for a given scripthash. +func (bc *Blockchain) GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) { + var siMap = make(map[string]*StorageItem) + var err error + + saveToMap := func(k, v []byte) { + if err != nil { + return + } + r := io.NewBinReaderFromBuf(v) + si := &StorageItem{} + si.DecodeBinary(r) + if r.Err != nil { + err = r.Err + return + } + + // Cut prefix and hash. + siMap[string(k[21:])] = si + } + bc.memStore.Seek(storage.AppendPrefix(storage.STStorage, hash.BytesReverse()), saveToMap) + bc.Store.Seek(storage.AppendPrefix(storage.STStorage, hash.BytesReverse()), saveToMap) + if err != nil { + return nil, err + } + return siMap, nil +} + // GetBlock returns a Block by the given hash. func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) { block, err := getBlockFromStore(bc.memStore, hash) diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 53f1d6400..85724a4cd 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -23,6 +23,8 @@ type Blockchainer interface { HasTransaction(util.Uint256) bool GetAssetState(util.Uint256) *AssetState GetAccountState(util.Uint160) *AccountState + GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem + GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) References(t *transaction.Transaction) map[transaction.Input]*transaction.Output Feer // fee interface diff --git a/pkg/core/storage_item.go b/pkg/core/storage_item.go new file mode 100644 index 000000000..2e6166a8b --- /dev/null +++ b/pkg/core/storage_item.go @@ -0,0 +1,64 @@ +package core + +import ( + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// StorageItem is the value to be stored with read-only flag. +type StorageItem struct { + Value []byte + IsConst bool +} + +// makeStorageItemKey returns a key used to store StorageItem in the DB. +func makeStorageItemKey(scripthash util.Uint160, key []byte) []byte { + return storage.AppendPrefix(storage.STStorage, append(scripthash.BytesReverse(), key...)) +} + +// getStorageItemFromStore returns StorageItem if it exists in the given Store. +func getStorageItemFromStore(s storage.Store, scripthash util.Uint160, key []byte) *StorageItem { + b, err := s.Get(makeStorageItemKey(scripthash, key)) + if err != nil { + return nil + } + r := io.NewBinReaderFromBuf(b) + + si := &StorageItem{} + si.DecodeBinary(r) + if r.Err != nil { + return nil + } + + return si +} + +// putStorageItemIntoStore puts given StorageItem for given script with given +// key into the given Store. +func putStorageItemIntoStore(s storage.Store, scripthash util.Uint160, key []byte, si *StorageItem) error { + buf := io.NewBufBinWriter() + si.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + return s.Put(makeStorageItemKey(scripthash, key), buf.Bytes()) +} + +// deleteStorageItemInStore drops storage item for the given script with the +// given key from the Store. +func deleteStorageItemInStore(s storage.Store, scripthash util.Uint160, key []byte) error { + return s.Delete(makeStorageItemKey(scripthash, key)) +} + +// EncodeBinary implements Serializable interface. +func (si *StorageItem) EncodeBinary(w *io.BinWriter) { + w.WriteBytes(si.Value) + w.WriteLE(si.IsConst) +} + +// DecodeBinary implements Serializable interface. +func (si *StorageItem) DecodeBinary(r *io.BinReader) { + si.Value = r.ReadBytes() + r.ReadLE(&si.IsConst) +} diff --git a/pkg/core/storage_item_test.go b/pkg/core/storage_item_test.go new file mode 100644 index 000000000..d555cdd21 --- /dev/null +++ b/pkg/core/storage_item_test.go @@ -0,0 +1,26 @@ +package core + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/assert" +) + +func TestPutGetDeleteStorageItem(t *testing.T) { + s := storage.NewMemoryStore() + si := &StorageItem{ + Value: []byte("smth"), + } + key := []byte("key") + cHash, err := util.Uint160DecodeBytes([]byte("abcdefghijklmnopqrst")) + assert.Nil(t, err) + assert.NoError(t, putStorageItemIntoStore(s, cHash, key, si)) + siRead := getStorageItemFromStore(s, cHash, key) + assert.NotNil(t, siRead) + assert.Equal(t, si, siRead) + assert.NoError(t, deleteStorageItemInStore(s, cHash, key)) + siRead2 := getStorageItemFromStore(s, cHash, key) + assert.Nil(t, siRead2) +} diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 09fafb87d..0aeec48d9 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -69,6 +69,12 @@ func (chain testChain) GetAssetState(util.Uint256) *core.AssetState { func (chain testChain) GetAccountState(util.Uint160) *core.AccountState { panic("TODO") } +func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem { + panic("TODO") +} +func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*core.StorageItem, error) { + panic("TODO") +} func (chain testChain) CurrentHeaderHash() util.Uint256 { return util.Uint256{} } From 19fd7f844ebc230431b115f9b6fc57bc562855ff Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 14:16:53 +0300 Subject: [PATCH 23/31] core: add GetUnspentCoinState() for future interops --- pkg/core/blockchain.go | 9 +++++++++ pkg/core/blockchainer.go | 1 + pkg/network/helper_test.go | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d8051e9c0..ac37762ba 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -740,6 +740,15 @@ func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState { return as } +// GetUnspentCoinState returns unspent coin state for given tx hash. +func (bc *Blockchain) GetUnspentCoinState(hash util.Uint256) *UnspentCoinState { + ucs, err := getUnspentCoinStateFromStore(bc.memStore, hash) + if err != nil { + ucs, _ = getUnspentCoinStateFromStore(bc.Store, hash) + } + return ucs +} + // GetConfig returns the config stored in the blockchain func (bc *Blockchain) GetConfig() config.ProtocolConfiguration { return bc.config diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 85724a4cd..7038617b1 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -26,6 +26,7 @@ type Blockchainer interface { GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) + GetUnspentCoinState(util.Uint256) *UnspentCoinState References(t *transaction.Transaction) map[transaction.Input]*transaction.Output Feer // fee interface Verify(t *transaction.Transaction) error diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 0aeec48d9..1b851b226 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -91,6 +91,10 @@ func (chain testChain) GetTransaction(util.Uint256) (*transaction.Transaction, u panic("TODO") } +func (chain testChain) GetUnspentCoinState(util.Uint256) *core.UnspentCoinState { + panic("TODO") +} + func (chain testChain) GetMemPool() core.MemPool { panic("TODO") } From 8266a5ce19b2e0a24a7539d1740a24549dc645dd Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 14:22:53 +0300 Subject: [PATCH 24/31] core: export GetContractState/GetScriptHashesForVerifying via Blockchainer These are gonna be used by interops and are also useful in general. --- pkg/core/blockchainer.go | 2 ++ pkg/network/helper_test.go | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 7038617b1..deb295fba 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -15,6 +15,7 @@ type Blockchainer interface { BlockHeight() uint32 HeaderHeight() uint32 GetBlock(hash util.Uint256) (*Block, error) + GetContractState(hash util.Uint160) *ContractState GetHeaderHash(int) util.Uint256 GetHeader(hash util.Uint256) (*Header, error) CurrentHeaderHash() util.Uint256 @@ -23,6 +24,7 @@ type Blockchainer interface { HasTransaction(util.Uint256) bool GetAssetState(util.Uint256) *AssetState GetAccountState(util.Uint160) *AccountState + GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 1b851b226..f42cb597c 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -56,6 +56,9 @@ func (chain testChain) HeaderHeight() uint32 { func (chain testChain) GetBlock(hash util.Uint256) (*core.Block, error) { panic("TODO") } +func (chain testChain) GetContractState(hash util.Uint160) *core.ContractState { + panic("TODO") +} func (chain testChain) GetHeaderHash(int) util.Uint256 { return util.Uint256{} } @@ -69,6 +72,9 @@ func (chain testChain) GetAssetState(util.Uint256) *core.AssetState { func (chain testChain) GetAccountState(util.Uint160) *core.AccountState { panic("TODO") } +func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) { + panic("TODO") +} func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem { panic("TODO") } From 667f346c04fac0adc8ea788dde949f2d91044a6c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 14:24:00 +0300 Subject: [PATCH 25/31] core: improve error message in AddBlock test --- pkg/core/blockchain_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 9938ac34a..27fc544e5 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -102,7 +102,7 @@ func TestGetBlock(t *testing.T) { for i := 0; i < len(blocks); i++ { block, err := bc.GetBlock(blocks[i].Hash()) if err != nil { - t.Fatal(err) + t.Fatalf("can't get block %d: %s, attempt %d", i, err, j) } assert.Equal(t, blocks[i].Index, block.Index) assert.Equal(t, blocks[i].Hash(), block.Hash()) From 74590551c4f6144ec31fd3e25da9c0ee0033a304 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 17:00:11 +0300 Subject: [PATCH 26/31] core: add some interops This also changes Verify to VerifyTx and VerifyWitnesses, because there is a need to pass a block for some interop functions. --- pkg/core/blockchain.go | 19 +- pkg/core/blockchainer.go | 2 +- pkg/core/interop_neo.go | 738 +++++++++++++++++++++++++++++++++++++ pkg/core/interop_system.go | 583 +++++++++++++++++++++++++++++ pkg/core/interops.go | 218 +++++++++++ pkg/network/helper_test.go | 2 +- pkg/network/server.go | 2 +- 7 files changed, 1555 insertions(+), 9 deletions(-) create mode 100644 pkg/core/interop_neo.go create mode 100644 pkg/core/interop_system.go create mode 100644 pkg/core/interops.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index ac37762ba..94e74c19d 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -215,7 +215,7 @@ func (bc *Blockchain) AddBlock(block *Block) error { return fmt.Errorf("block %s is invalid: %s", block.Hash().ReverseString(), err) } for _, tx := range block.Transactions { - err := bc.Verify(tx) + err := bc.VerifyTx(tx, block) if err != nil { return fmt.Errorf("transaction %s failed to verify: %s", tx.Hash().ReverseString(), err) } @@ -816,9 +816,11 @@ func (bc *Blockchain) GetMemPool() MemPool { return bc.memPool } -// Verify verifies whether a transaction is bonafide or not. +// VerifyTx verifies whether a transaction is bonafide or not. Block parameter +// is used for easy interop access and can be omitted for transactions that are +// not yet added into any block. // Golang implementation of Verify method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L270). -func (bc *Blockchain) Verify(t *transaction.Transaction) error { +func (bc *Blockchain) VerifyTx(t *transaction.Transaction, block *Block) error { if io.GetVarSize(t) > transaction.MaxTransactionSize { return errors.Errorf("invalid transaction size = %d. It shoud be less then MaxTransactionSize = %d", io.GetVarSize(t), transaction.MaxTransactionSize) } @@ -844,7 +846,7 @@ func (bc *Blockchain) Verify(t *transaction.Transaction) error { } } - return bc.VerifyWitnesses(t) + return bc.VerifyWitnesses(t, block) } func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool { @@ -1070,10 +1072,12 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([ // VerifyWitnesses verify the scripts (witnesses) that come with a given // transaction. It can reorder them by ScriptHash, because that's required to -// match a slice of script hashes from the Blockchain. +// match a slice of script hashes from the Blockchain. Block parameter +// is used for easy interop access and can be omitted for transactions that are +// not yet added into any block. // Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). // Unfortunately the IVerifiable interface could not be implemented because we can't move the References method in blockchain.go to the transaction.go file -func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error { +func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction, block *Block) error { hashes, err := bc.GetScriptHashesForVerifying(t) if err != nil { return err @@ -1110,6 +1114,9 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error { } return cs.Script }) + systemInterop := newInteropContext(0, bc, block, t) + vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) + vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) vm.LoadScript(verification) vm.LoadScript(witnesses[i].InvocationScript) vm.Run() diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index deb295fba..1409a2c40 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -31,6 +31,6 @@ type Blockchainer interface { GetUnspentCoinState(util.Uint256) *UnspentCoinState References(t *transaction.Transaction) map[transaction.Input]*transaction.Output Feer // fee interface - Verify(t *transaction.Transaction) error + VerifyTx(*transaction.Transaction, *Block) error GetMemPool() MemPool } diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go new file mode 100644 index 000000000..19d9247cf --- /dev/null +++ b/pkg/core/interop_neo.go @@ -0,0 +1,738 @@ +package core + +import ( + "errors" + "fmt" + "math" + + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + gherr "github.com/pkg/errors" +) + +const ( + // MaxContractScriptSize is the maximum script size for a contract. + MaxContractScriptSize = 1024 * 1024 + // MaxContractParametersNum is the maximum number of parameters for a contract. + MaxContractParametersNum = 252 + // MaxContractStringLen is the maximum length for contract metadata strings. + MaxContractStringLen = 252 + // MaxAssetNameLen is the maximum length of asset name. + MaxAssetNameLen = 1024 + // MaxAssetPrecision is the maximum precision of asset. + MaxAssetPrecision = 8 + // BlocksPerYear is a multiplier for asset renewal. + BlocksPerYear = 2000000 + // DefaultAssetLifetime is the default lifetime of an asset (which differs + // from assets created by register tx). + DefaultAssetLifetime = 1 + BlocksPerYear +) + +// txInOut is used to pushed one key-value pair from References() onto the stack. +type txInOut struct { + in transaction.Input + out transaction.Output +} + +// headerGetVersion returns version from the header. +func (ic *interopContext) headerGetVersion(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.Version) + return nil +} + +// headerGetConsensusData returns consensus data from the header. +func (ic *interopContext) headerGetConsensusData(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.ConsensusData) + return nil +} + +// headerGetMerkleRoot returns version from the header. +func (ic *interopContext) headerGetMerkleRoot(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.MerkleRoot.BytesReverse()) + return nil +} + +// headerGetNextConsensus returns version from the header. +func (ic *interopContext) headerGetNextConsensus(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.NextConsensus.BytesReverse()) + return nil +} + +// txGetAttributes returns current transaction attributes. +func (ic *interopContext) txGetAttributes(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + if len(tx.Attributes) > vm.MaxArraySize { + return errors.New("too many attributes") + } + attrs := make([]vm.StackItem, 0, len(tx.Attributes)) + for _, attr := range tx.Attributes { + attrs = append(attrs, vm.NewInteropItem(attr)) + } + v.Estack().PushVal(attrs) + return nil +} + +// txGetInputs returns current transaction inputs. +func (ic *interopContext) txGetInputs(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + if len(tx.Inputs) > vm.MaxArraySize { + return errors.New("too many inputs") + } + inputs := make([]vm.StackItem, 0, len(tx.Inputs)) + for _, input := range tx.Inputs { + inputs = append(inputs, vm.NewInteropItem(input)) + } + v.Estack().PushVal(inputs) + return nil +} + +// txGetOutputs returns current transaction outputs. +func (ic *interopContext) txGetOutputs(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + if len(tx.Outputs) > vm.MaxArraySize { + return errors.New("too many outputs") + } + outputs := make([]vm.StackItem, 0, len(tx.Outputs)) + for _, output := range tx.Outputs { + outputs = append(outputs, vm.NewInteropItem(output)) + } + v.Estack().PushVal(outputs) + return nil +} + +// txGetReferences returns current transaction references. +func (ic *interopContext) txGetReferences(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return fmt.Errorf("type mismatch: %T is not a Transaction", txInterface) + } + refs := ic.bc.References(tx) + if len(refs) > vm.MaxArraySize { + return errors.New("too many references") + } + + stackrefs := make([]vm.StackItem, 0, len(refs)) + for k, v := range refs { + tio := txInOut{k, *v} + stackrefs = append(stackrefs, vm.NewInteropItem(tio)) + } + v.Estack().PushVal(stackrefs) + return nil +} + +// txGetType returns current transaction type. +func (ic *interopContext) txGetType(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + v.Estack().PushVal(int(tx.Type)) + return nil +} + +// txGetUnspentCoins returns current transaction unspent coins. +func (ic *interopContext) txGetUnspentCoins(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + ucs := ic.bc.GetUnspentCoinState(tx.Hash()) + if ucs == nil { + return errors.New("no unspent coin state found") + } + v.Estack().PushVal(vm.NewInteropItem(ucs)) + return nil +} + +// txGetWitnesses returns current transaction witnesses. +func (ic *interopContext) txGetWitnesses(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + if len(tx.Scripts) > vm.MaxArraySize { + return errors.New("too many outputs") + } + scripts := make([]vm.StackItem, 0, len(tx.Scripts)) + for _, script := range tx.Scripts { + scripts = append(scripts, vm.NewInteropItem(script)) + } + v.Estack().PushVal(scripts) + return nil +} + +// popInputFromVM returns transaction.Input from the first estack element. +func popInputFromVM(v *vm.VM) (*transaction.Input, error) { + inInterface := v.Estack().Pop().Value() + input, ok := inInterface.(*transaction.Input) + if !ok { + txio, ok := inInterface.(txInOut) + if !ok { + return nil, fmt.Errorf("type mismatch: %T is not an Input or txInOut", inInterface) + } + input = &txio.in + } + return input, nil +} + +// inputGetHash returns hash from the given input. +func (ic *interopContext) inputGetHash(v *vm.VM) error { + input, err := popInputFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(input.PrevHash.Bytes()) + return nil +} + +// inputGetIndex returns index from the given input. +func (ic *interopContext) inputGetIndex(v *vm.VM) error { + input, err := popInputFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(input.PrevIndex) + return nil +} + +// popOutputFromVM returns transaction.Input from the first estack element. +func popOutputFromVM(v *vm.VM) (*transaction.Output, error) { + outInterface := v.Estack().Pop().Value() + output, ok := outInterface.(*transaction.Output) + if !ok { + txio, ok := outInterface.(txInOut) + if !ok { + return nil, fmt.Errorf("type mismatch: %T is not an Output or txInOut", outInterface) + } + output = &txio.out + } + return output, nil +} + +// outputGetAssetId returns asset ID from the given output. +func (ic *interopContext) outputGetAssetID(v *vm.VM) error { + output, err := popOutputFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(output.AssetID.Bytes()) + return nil +} + +// outputGetScriptHash returns scripthash from the given output. +func (ic *interopContext) outputGetScriptHash(v *vm.VM) error { + output, err := popOutputFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(output.ScriptHash.Bytes()) + return nil +} + +// outputGetValue returns value (amount) from the given output. +func (ic *interopContext) outputGetValue(v *vm.VM) error { + output, err := popOutputFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(int64(output.Amount)) + return nil +} + +// attrGetData returns tx attribute data. +func (ic *interopContext) attrGetData(v *vm.VM) error { + attrInterface := v.Estack().Pop().Value() + attr, ok := attrInterface.(*transaction.Attribute) + if !ok { + return fmt.Errorf("%T is not an attribute", attr) + } + v.Estack().PushVal(attr.Data) + return nil +} + +// attrGetData returns tx attribute usage field. +func (ic *interopContext) attrGetUsage(v *vm.VM) error { + attrInterface := v.Estack().Pop().Value() + attr, ok := attrInterface.(*transaction.Attribute) + if !ok { + return fmt.Errorf("%T is not an attribute", attr) + } + v.Estack().PushVal(int(attr.Usage)) + return nil +} + +// bcGetAccount returns or creates an account. +func (ic *interopContext) bcGetAccount(v *vm.VM) error { + accbytes := v.Estack().Pop().Bytes() + acchash, err := util.Uint160DecodeBytes(accbytes) + if err != nil { + return err + } + acc := ic.bc.GetAccountState(acchash) + if acc == nil { + acc = NewAccountState(acchash) + } + v.Estack().PushVal(vm.NewInteropItem(acc)) + return nil +} + +// bcGetAsset returns an asset. +func (ic *interopContext) bcGetAsset(v *vm.VM) error { + asbytes := v.Estack().Pop().Bytes() + ashash, err := util.Uint256DecodeBytes(asbytes) + if err != nil { + return err + } + as := ic.bc.GetAssetState(ashash) + if as == nil { + return errors.New("asset not found") + } + v.Estack().PushVal(vm.NewInteropItem(as)) + return nil +} + +// accountGetBalance returns balance for a given account. +func (ic *interopContext) accountGetBalance(v *vm.VM) error { + accInterface := v.Estack().Pop().Value() + acc, ok := accInterface.(*AccountState) + if !ok { + return fmt.Errorf("%T is not an account state", acc) + } + asbytes := v.Estack().Pop().Bytes() + ashash, err := util.Uint256DecodeBytes(asbytes) + if err != nil { + return err + } + balance, ok := acc.Balances[ashash] + if !ok { + balance = util.Fixed8(0) + } + v.Estack().PushVal(int64(balance)) + return nil +} + +// accountGetScriptHash returns script hash of a given account. +func (ic *interopContext) accountGetScriptHash(v *vm.VM) error { + accInterface := v.Estack().Pop().Value() + acc, ok := accInterface.(*AccountState) + if !ok { + return fmt.Errorf("%T is not an account state", acc) + } + v.Estack().PushVal(acc.ScriptHash.Bytes()) + return nil +} + +// accountGetVotes returns votes of a given account. +func (ic *interopContext) accountGetVotes(v *vm.VM) error { + accInterface := v.Estack().Pop().Value() + acc, ok := accInterface.(*AccountState) + if !ok { + return fmt.Errorf("%T is not an account state", acc) + } + if len(acc.Votes) > vm.MaxArraySize { + return errors.New("too many votes") + } + votes := make([]vm.StackItem, 0, len(acc.Votes)) + for _, key := range acc.Votes { + votes = append(votes, vm.NewByteArrayItem(key.Bytes())) + } + v.Estack().PushVal(votes) + return nil +} + +// accountIsStandard checks whether given account is standard. +func (ic *interopContext) accountIsStandard(v *vm.VM) error { + accbytes := v.Estack().Pop().Bytes() + acchash, err := util.Uint160DecodeBytes(accbytes) + if err != nil { + return err + } + contract := ic.bc.GetContractState(acchash) + res := contract == nil || vm.IsStandardContract(contract.Script) + v.Estack().PushVal(res) + return nil +} + +/* +// storageFind finds stored key-value pair. +func (ic *interopContext) storageFind(v *vm.VM) error { + stcInterface := v.Estack().Pop().Value() + stc, ok := stcInterface.(*StorageContext) + if !ok { + return fmt.Errorf("%T is not a StorageContext", stcInterface) + } + err := ic.checkStorageContext(stc) + if err != nil { + return err + } + prefix := string(v.Estack().Pop().Bytes()) + siMap, err := ic.bc.GetStorageItems(stc.ScriptHash) + if err != nil { + return err + } + for k, v := range siMap { + if strings.HasPrefix(k, prefix) { + _ = v + panic("TODO") + } + } + + return nil +} +*/ +// createContractStateFromVM pops all contract state elements from the VM +// evaluation stack, does a lot of checks and returns ContractState if it +// succeedes. +func (ic *interopContext) createContractStateFromVM(v *vm.VM) (*ContractState, error) { + if ic.trigger != 0x10 { + return nil, errors.New("can't create contract when not triggered by an application") + } + script := v.Estack().Pop().Bytes() + if len(script) > MaxContractScriptSize { + return nil, errors.New("the script is too big") + } + paramBytes := v.Estack().Pop().Bytes() + if len(paramBytes) > MaxContractParametersNum { + return nil, errors.New("too many parameters for a script") + } + paramList := make([]smartcontract.ParamType, len(paramBytes)) + for k, v := range paramBytes { + paramList[k] = smartcontract.ParamType(v) + } + retType := smartcontract.ParamType(v.Estack().Pop().BigInt().Int64()) + properties := smartcontract.PropertyState(v.Estack().Pop().BigInt().Int64()) + name := v.Estack().Pop().Bytes() + if len(name) > MaxContractStringLen { + return nil, errors.New("too big name") + } + version := v.Estack().Pop().Bytes() + if len(version) > MaxContractStringLen { + return nil, errors.New("too big version") + } + author := v.Estack().Pop().Bytes() + if len(author) > MaxContractStringLen { + return nil, errors.New("too big author") + } + email := v.Estack().Pop().Bytes() + if len(email) > MaxContractStringLen { + return nil, errors.New("too big email") + } + desc := v.Estack().Pop().Bytes() + if len(desc) > MaxContractStringLen { + return nil, errors.New("too big description") + } + contract := &ContractState{ + Script: script, + ParamList: paramList, + ReturnType: retType, + Properties: properties, + Name: string(name), + CodeVersion: string(version), + Author: string(author), + Email: string(email), + Description: string(desc), + } + return contract, nil +} + +// contractCreate creates a contract. +func (ic *interopContext) contractCreate(v *vm.VM) error { + newcontract, err := ic.createContractStateFromVM(v) + if err != nil { + return nil + } + contract := ic.bc.GetContractState(newcontract.ScriptHash()) + if contract == nil { + contract = newcontract + err := putContractStateIntoStore(ic.mem, contract) + if err != nil { + return err + } + } + v.Estack().PushVal(vm.NewInteropItem(contract)) + return nil +} + +// contractGetScript returns a script associated with a contract. +func (ic *interopContext) contractGetScript(v *vm.VM) error { + csInterface := v.Estack().Pop().Value() + cs, ok := csInterface.(*ContractState) + if !ok { + return fmt.Errorf("%T is not a contract state", cs) + } + v.Estack().PushVal(cs.Script) + return nil +} + +// contractIsPayable returns whether contract is payable. +func (ic *interopContext) contractIsPayable(v *vm.VM) error { + csInterface := v.Estack().Pop().Value() + cs, ok := csInterface.(*ContractState) + if !ok { + return fmt.Errorf("%T is not a contract state", cs) + } + v.Estack().PushVal(cs.IsPayable()) + return nil +} + +// contractMigrate migrates a contract. +func (ic *interopContext) contractMigrate(v *vm.VM) error { + newcontract, err := ic.createContractStateFromVM(v) + if err != nil { + return nil + } + contract := ic.bc.GetContractState(newcontract.ScriptHash()) + if contract == nil { + contract = newcontract + err := putContractStateIntoStore(ic.mem, contract) + if err != nil { + return err + } + if contract.HasStorage() { + hash := getContextScriptHash(v, 0) + siMap, err := ic.bc.GetStorageItems(hash) + if err != nil { + return err + } + for k, v := range siMap { + v.IsConst = false + _ = putStorageItemIntoStore(ic.mem, hash, []byte(k), v) + } + } + } + v.Estack().PushVal(vm.NewInteropItem(contract)) + return ic.contractDestroy(v) +} + +// assetCreate creates an asset. +func (ic *interopContext) assetCreate(v *vm.VM) error { + if ic.trigger != 0x10 { + return errors.New("can't create asset when not triggered by an application") + } + atype := transaction.AssetType(v.Estack().Pop().BigInt().Int64()) + switch atype { + case transaction.Currency, transaction.Share, transaction.Invoice, transaction.Token: + // ok + default: + return fmt.Errorf("wrong asset type: %x", atype) + } + name := string(v.Estack().Pop().Bytes()) + if len(name) > MaxAssetNameLen { + return errors.New("too big name") + } + amount := util.Fixed8(v.Estack().Pop().BigInt().Int64()) + if amount == util.Fixed8(0) { + return errors.New("asset amount can't be zero") + } + if amount < -util.Satoshi() { + return errors.New("asset amount can't be negative (except special -Satoshi value") + } + if atype == transaction.Invoice && amount != -util.Satoshi() { + return errors.New("invoice assets can only have -Satoshi amount") + } + precision := byte(v.Estack().Pop().BigInt().Int64()) + if precision > MaxAssetPrecision { + return fmt.Errorf("can't have asset precision of more than %d", MaxAssetPrecision) + } + if atype == transaction.Share && precision != 0 { + return errors.New("share assets can only have zero precision") + } + if amount != -util.Satoshi() && (int64(amount)%int64(math.Pow10(int(MaxAssetPrecision-precision))) != 0) { + return errors.New("given asset amount has fractional component") + } + owner := &keys.PublicKey{} + err := owner.DecodeBytes(v.Estack().Pop().Bytes()) + if err != nil { + return gherr.Wrap(err, "failed to get owner key") + } + if owner.IsInfinity() { + return errors.New("can't have infinity as an owner key") + } + witnessOk, err := ic.checkKeyedWitness(owner) + if err != nil { + return err + } + if !witnessOk { + return errors.New("witness check didn't succeed") + } + admin, err := util.Uint160DecodeBytes(v.Estack().Pop().Bytes()) + if err != nil { + return gherr.Wrap(err, "failed to get admin") + } + issuer, err := util.Uint160DecodeBytes(v.Estack().Pop().Bytes()) + if err != nil { + return gherr.Wrap(err, "failed to get issuer") + } + asset := &AssetState{ + ID: ic.tx.Hash(), + AssetType: atype, + Name: name, + Amount: amount, + Precision: precision, + Owner: owner, + Admin: admin, + Issuer: issuer, + Expiration: ic.bc.BlockHeight() + DefaultAssetLifetime, + } + err = putAssetStateIntoStore(ic.mem, asset) + if err != nil { + return gherr.Wrap(err, "failed to store asset") + } + v.Estack().PushVal(vm.NewInteropItem(asset)) + return nil +} + +// assetGetAdmin returns asset admin. +func (ic *interopContext) assetGetAdmin(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(as.Admin.Bytes()) + return nil +} + +// assetGetAmount returns the overall amount of asset available. +func (ic *interopContext) assetGetAmount(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(int64(as.Amount)) + return nil +} + +// assetGetAssetId returns the id of an asset. +func (ic *interopContext) assetGetAssetID(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(as.ID.Bytes()) + return nil +} + +// assetGetAssetType returns type of an asset. +func (ic *interopContext) assetGetAssetType(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(int(as.AssetType)) + return nil +} + +// assetGetAvailable returns available (not yet issued) amount of asset. +func (ic *interopContext) assetGetAvailable(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(int(as.Available)) + return nil +} + +// assetGetIssuer returns issuer of an asset. +func (ic *interopContext) assetGetIssuer(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(as.Issuer.Bytes()) + return nil +} + +// assetGetOwner returns owner of an asset. +func (ic *interopContext) assetGetOwner(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(as.Owner.Bytes()) + return nil +} + +// assetGetPrecision returns precision used to measure this asset. +func (ic *interopContext) assetGetPrecision(v *vm.VM) error { + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + v.Estack().PushVal(int(as.Precision)) + return nil +} + +// assetRenew updates asset expiration date. +func (ic *interopContext) assetRenew(v *vm.VM) error { + if ic.trigger != 0x10 { + return errors.New("can't create asset when not triggered by an application") + } + asInterface := v.Estack().Pop().Value() + as, ok := asInterface.(*AssetState) + if !ok { + return fmt.Errorf("%T is not an asset state", as) + } + years := byte(v.Estack().Pop().BigInt().Int64()) + // Not sure why C# code regets an asset from the Store, but we also do it. + asset := ic.bc.GetAssetState(as.ID) + if asset == nil { + return errors.New("can't renew non-existent asset") + } + if asset.Expiration < ic.bc.BlockHeight()+1 { + asset.Expiration = ic.bc.BlockHeight() + 1 + } + expiration := uint64(asset.Expiration) + uint64(years)*BlocksPerYear + if expiration > math.MaxUint32 { + expiration = math.MaxUint32 + } + asset.Expiration = uint32(expiration) + err := putAssetStateIntoStore(ic.mem, asset) + if err != nil { + return gherr.Wrap(err, "failed to store asset") + } + v.Estack().PushVal(expiration) + return nil +} diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go new file mode 100644 index 000000000..5401bac49 --- /dev/null +++ b/pkg/core/interop_system.go @@ -0,0 +1,583 @@ +package core + +import ( + "errors" + "fmt" + "math" + + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + gherr "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +const ( + // MaxStorageKeyLen is the maximum length of a key for storage items. + MaxStorageKeyLen = 1024 +) + +// StorageContext contains storing script hash and read/write flag, it's used as +// a context for storage manipulation functions. +type StorageContext struct { + ScriptHash util.Uint160 + ReadOnly bool +} + +// getBlockHashFromElement converts given vm.Element to block hash using given +// Blockchainer if needed. Interop functions accept both block numbers and +// block hashes as parameters, thus this function is needed. +func getBlockHashFromElement(bc Blockchainer, element *vm.Element) (util.Uint256, error) { + var hash util.Uint256 + hashbytes := element.Bytes() + if len(hashbytes) <= 5 { + hashint := element.BigInt().Int64() + if hashint < 0 || hashint > math.MaxUint32 { + return hash, errors.New("bad block index") + } + hash = bc.GetHeaderHash(int(hashint)) + } else { + return util.Uint256DecodeReverseBytes(hashbytes) + } + return hash, nil +} + +// bcGetBlock returns current block. +func (ic *interopContext) bcGetBlock(v *vm.VM) error { + hash, err := getBlockHashFromElement(ic.bc, v.Estack().Pop()) + if err != nil { + return err + } + block, err := ic.bc.GetBlock(hash) + if err != nil { + v.Estack().PushVal([]byte{}) + } else { + v.Estack().PushVal(vm.NewInteropItem(block)) + } + return nil +} + +// bcGetContract returns contract. +func (ic *interopContext) bcGetContract(v *vm.VM) error { + hashbytes := v.Estack().Pop().Bytes() + hash, err := util.Uint160DecodeBytes(hashbytes) + if err != nil { + return err + } + cs := ic.bc.GetContractState(hash) + if cs == nil { + v.Estack().PushVal([]byte{}) + } else { + v.Estack().PushVal(vm.NewInteropItem(cs)) + } + return nil +} + +// bcGetHeader returns block header. +func (ic *interopContext) bcGetHeader(v *vm.VM) error { + hash, err := getBlockHashFromElement(ic.bc, v.Estack().Pop()) + if err != nil { + return err + } + header, err := ic.bc.GetHeader(hash) + if err != nil { + v.Estack().PushVal([]byte{}) + } else { + v.Estack().PushVal(vm.NewInteropItem(header)) + } + return nil +} + +// bcGetHeight returns blockchain height. +func (ic *interopContext) bcGetHeight(v *vm.VM) error { + v.Estack().PushVal(ic.bc.BlockHeight()) + return nil +} + +// getTransactionAndHeight gets parameter from the vm evaluation stack and +// returns transaction and its height if it's present in the blockchain. +func getTransactionAndHeight(bc Blockchainer, v *vm.VM) (*transaction.Transaction, uint32, error) { + hashbytes := v.Estack().Pop().Bytes() + hash, err := util.Uint256DecodeReverseBytes(hashbytes) + if err != nil { + return nil, 0, err + } + return bc.GetTransaction(hash) +} + +// bcGetTransaction returns transaction. +func (ic *interopContext) bcGetTransaction(v *vm.VM) error { + tx, _, err := getTransactionAndHeight(ic.bc, v) + if err != nil { + return err + } + v.Estack().PushVal(vm.NewInteropItem(tx)) + return nil +} + +// bcGetTransactionHeight returns transaction height. +func (ic *interopContext) bcGetTransactionHeight(v *vm.VM) error { + _, h, err := getTransactionAndHeight(ic.bc, v) + if err != nil { + return err + } + v.Estack().PushVal(h) + return nil +} + +// popHeaderFromVM returns pointer to Header or error. It's main feature is +// proper treatment of Block structure, because C# code implicitly assumes +// that header APIs can also operate on blocks. +func popHeaderFromVM(v *vm.VM) (*Header, error) { + iface := v.Estack().Pop().Value() + header, ok := iface.(*Header) + if !ok { + block, ok := iface.(*Block) + if !ok { + return nil, errors.New("value is not a header or block") + } + return block.Header(), nil + } + return header, nil +} + +// headerGetIndex returns block index from the header. +func (ic *interopContext) headerGetIndex(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.Index) + return nil +} + +// headerGetHash returns header hash of the passed header. +func (ic *interopContext) headerGetHash(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.Hash().BytesReverse()) + return nil +} + +// headerGetPrevHash returns previous header hash of the passed header. +func (ic *interopContext) headerGetPrevHash(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.PrevHash.BytesReverse()) + return nil +} + +// headerGetTimestamp returns timestamp of the passed header. +func (ic *interopContext) headerGetTimestamp(v *vm.VM) error { + header, err := popHeaderFromVM(v) + if err != nil { + return err + } + v.Estack().PushVal(header.Timestamp) + return nil +} + +// blockGetTransactionCount returns transactions count in the given block. +func (ic *interopContext) blockGetTransactionCount(v *vm.VM) error { + blockInterface := v.Estack().Pop().Value() + block, ok := blockInterface.(*Block) + if !ok { + return errors.New("value is not a block") + } + v.Estack().PushVal(len(block.Transactions)) + return nil +} + +// blockGetTransactions returns transactions from the given block. +func (ic *interopContext) blockGetTransactions(v *vm.VM) error { + blockInterface := v.Estack().Pop().Value() + block, ok := blockInterface.(*Block) + if !ok { + return errors.New("value is not a block") + } + if len(block.Transactions) > vm.MaxArraySize { + return errors.New("too many transactions") + } + txes := make([]vm.StackItem, 0, len(block.Transactions)) + for _, tx := range block.Transactions { + txes = append(txes, vm.NewInteropItem(tx)) + } + v.Estack().PushVal(txes) + return nil +} + +// blockGetTransaction returns transaction with the given number from the given +// block. +func (ic *interopContext) blockGetTransaction(v *vm.VM) error { + blockInterface := v.Estack().Pop().Value() + block, ok := blockInterface.(*Block) + if !ok { + return errors.New("value is not a block") + } + index := v.Estack().Pop().BigInt().Int64() + if index < 0 || index >= int64(len(block.Transactions)) { + return errors.New("wrong transaction index") + } + tx := block.Transactions[index] + v.Estack().PushVal(vm.NewInteropItem(tx)) + return nil +} + +// txGetHash returns transaction's hash. +func (ic *interopContext) txGetHash(v *vm.VM) error { + txInterface := v.Estack().Pop().Value() + tx, ok := txInterface.(*transaction.Transaction) + if !ok { + return errors.New("value is not a transaction") + } + v.Estack().PushVal(tx.Hash().BytesReverse()) + return nil +} + +// engineGetScriptContainer returns transaction that contains the script being +// run. +func (ic *interopContext) engineGetScriptContainer(v *vm.VM) error { + v.Estack().PushVal(vm.NewInteropItem(ic.tx)) + return nil +} + +// pushContextScriptHash returns script hash of the invocation stack element +// number n. +func getContextScriptHash(v *vm.VM, n int) util.Uint160 { + ctxIface := v.Istack().Peek(n).Value() + ctx := ctxIface.(*vm.Context) + return hash.Hash160(ctx.Program()) +} + +// pushContextScriptHash pushes to evaluation stack the script hash of the +// invocation stack element number n. +func pushContextScriptHash(v *vm.VM, n int) error { + h := getContextScriptHash(v, n) + v.Estack().PushVal(h.Bytes()) + return nil +} + +// engineGetExecutingScriptHash returns executing script hash. +func (ic *interopContext) engineGetExecutingScriptHash(v *vm.VM) error { + return pushContextScriptHash(v, 0) +} + +// engineGetCallingScriptHash returns calling script hash. +func (ic *interopContext) engineGetCallingScriptHash(v *vm.VM) error { + return pushContextScriptHash(v, 1) +} + +// engineGetEntryScriptHash returns entry script hash. +func (ic *interopContext) engineGetEntryScriptHash(v *vm.VM) error { + return pushContextScriptHash(v, v.Istack().Len()-1) +} + +// runtimePlatform returns the name of the platform. +func (ic *interopContext) runtimePlatform(v *vm.VM) error { + v.Estack().PushVal([]byte("NEO")) + return nil +} + +// runtimeGetTrigger returns the script trigger. +func (ic *interopContext) runtimeGetTrigger(v *vm.VM) error { + v.Estack().PushVal(ic.trigger) + return nil +} + +// checkHashedWitness checks given hash against current list of script hashes +// for verifying in the interop context. +func (ic *interopContext) checkHashedWitness(hash util.Uint160) (bool, error) { + hashes, err := ic.bc.GetScriptHashesForVerifying(ic.tx) + if err != nil { + return false, gherr.Wrap(err, "failed to get script hashes") + } + for _, v := range hashes { + if hash.Equals(v) { + return true, nil + } + } + return false, nil +} + +// checkKeyedWitness checks hash of signature check contract with a given public +// key against current list of script hashes for verifying in the interop context. +func (ic *interopContext) checkKeyedWitness(key *keys.PublicKey) (bool, error) { + script, err := smartcontract.CreateSignatureRedeemScript(key) + if err != nil { + return false, gherr.Wrap(err, "failed to create signature script for a key") + } + return ic.checkHashedWitness(hash.Hash160(script)) +} + +// runtimeCheckWitness should check witnesses. +func (ic *interopContext) runtimeCheckWitness(v *vm.VM) error { + var res bool + var err error + + hashOrKey := v.Estack().Pop().Bytes() + hash, err := util.Uint160DecodeBytes(hashOrKey) + if err != nil { + key := &keys.PublicKey{} + err = key.DecodeBytes(hashOrKey) + if err != nil { + return errors.New("parameter given is neither a key nor a hash") + } + res, err = ic.checkKeyedWitness(key) + } else { + res, err = ic.checkHashedWitness(hash) + } + if err != nil { + return gherr.Wrap(err, "failed to check") + } + v.Estack().PushVal(res) + return nil +} + +// runtimeNotify should pass stack item to the notify plugin to handle it, but +// in neo-go the only meaningful thing to do here is to log. +func (ic *interopContext) runtimeNotify(v *vm.VM) error { + msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes()) + log.Infof("script %s notifies: %s", getContextScriptHash(v, 0), msg) + return nil +} + +// runtimeLog log the message passed. +func (ic *interopContext) runtimeLog(v *vm.VM) error { + msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes()) + log.Infof("script %s logs: %s", getContextScriptHash(v, 0), msg) + return nil +} + +// runtimeGetTime returns timestamp of the block being verified, or the latest +// one in the blockchain if no block is given to interopContext. +func (ic *interopContext) runtimeGetTime(v *vm.VM) error { + var header *Header + if ic.block == nil { + var err error + header, err = ic.bc.GetHeader(ic.bc.CurrentBlockHash()) + if err != nil { + return err + } + } else { + header = ic.block.Header() + } + v.Estack().PushVal(header.Timestamp) + return nil +} + +/* +// runtimeSerialize should serialize given stack item. +func (ic *interopContext) runtimeSerialize(v *vm.VM) error { + panic("TODO") +} + +// runtimeDeserialize should deserialize given stack item. +func (ic *interopContext) runtimeDeserialize(v *vm.VM) error { + panic("TODO") +} +*/ +func (ic *interopContext) checkStorageContext(stc *StorageContext) error { + contract := ic.bc.GetContractState(stc.ScriptHash) + if contract == nil { + return errors.New("no contract found") + } + if !contract.HasStorage() { + return errors.New("contract can't have storage") + } + return nil +} + +// storageDelete deletes stored key-value pair. +func (ic *interopContext) storageDelete(v *vm.VM) error { + if ic.trigger != 0x10 && ic.trigger != 0x11 { + return errors.New("can't delete when the trigger is not application") + } + stcInterface := v.Estack().Pop().Value() + stc, ok := stcInterface.(*StorageContext) + if !ok { + return fmt.Errorf("%T is not a StorageContext", stcInterface) + } + if stc.ReadOnly { + return errors.New("StorageContext is read only") + } + err := ic.checkStorageContext(stc) + if err != nil { + return err + } + key := v.Estack().Pop().Bytes() + si := getStorageItemFromStore(ic.mem, stc.ScriptHash, key) + if si == nil { + si = ic.bc.GetStorageItem(stc.ScriptHash, key) + } + if si != nil && si.IsConst { + return errors.New("storage item is constant") + } + return deleteStorageItemInStore(ic.mem, stc.ScriptHash, key) +} + +// storageGet returns stored key-value pair. +func (ic *interopContext) storageGet(v *vm.VM) error { + stcInterface := v.Estack().Pop().Value() + stc, ok := stcInterface.(*StorageContext) + if !ok { + return fmt.Errorf("%T is not a StorageContext", stcInterface) + } + err := ic.checkStorageContext(stc) + if err != nil { + return err + } + key := v.Estack().Pop().Bytes() + si := getStorageItemFromStore(ic.mem, stc.ScriptHash, key) + if si == nil { + si = ic.bc.GetStorageItem(stc.ScriptHash, key) + } + if si != nil && si.Value != nil { + v.Estack().PushVal(si.Value) + } else { + v.Estack().PushVal([]byte{}) + } + return nil +} + +// storageGetContext returns storage context (scripthash). +func (ic *interopContext) storageGetContext(v *vm.VM) error { + sc := &StorageContext{ + ScriptHash: getContextScriptHash(v, 0), + ReadOnly: false, + } + v.Estack().PushVal(vm.NewInteropItem(sc)) + return nil +} + +// storageGetReadOnlyContext returns read-only context (scripthash). +func (ic *interopContext) storageGetReadOnlyContext(v *vm.VM) error { + sc := &StorageContext{ + ScriptHash: getContextScriptHash(v, 0), + ReadOnly: true, + } + v.Estack().PushVal(vm.NewInteropItem(sc)) + return nil +} + +func (ic *interopContext) putWithContextAndFlags(stc *StorageContext, key []byte, value []byte, isConst bool) error { + if ic.trigger != 0x10 && ic.trigger != 0x11 { + return errors.New("can't delete when the trigger is not application") + } + if len(key) > MaxStorageKeyLen { + return errors.New("key is too big") + } + if stc.ReadOnly { + return errors.New("StorageContext is read only") + } + err := ic.checkStorageContext(stc) + if err != nil { + return err + } + si := getStorageItemFromStore(ic.mem, stc.ScriptHash, key) + if si == nil { + si = ic.bc.GetStorageItem(stc.ScriptHash, key) + if si == nil { + si = &StorageItem{} + } + } + if si.IsConst { + return errors.New("storage item exists and is read-only") + } + si.Value = value + si.IsConst = isConst + return putStorageItemIntoStore(ic.mem, stc.ScriptHash, key, si) +} + +// storagePutInternal is a unified implementation of storagePut and storagePutEx. +func (ic *interopContext) storagePutInternal(v *vm.VM, getFlag bool) error { + stcInterface := v.Estack().Pop().Value() + stc, ok := stcInterface.(*StorageContext) + if !ok { + return fmt.Errorf("%T is not a StorageContext", stcInterface) + } + key := v.Estack().Pop().Bytes() + value := v.Estack().Pop().Bytes() + var flag int + if getFlag { + flag = int(v.Estack().Pop().BigInt().Int64()) + } + return ic.putWithContextAndFlags(stc, key, value, flag == 1) +} + +// storagePut puts key-value pair into the storage. +func (ic *interopContext) storagePut(v *vm.VM) error { + return ic.storagePutInternal(v, false) +} + +// storagePutEx puts key-value pair with given flags into the storage. +func (ic *interopContext) storagePutEx(v *vm.VM) error { + return ic.storagePutInternal(v, true) +} + +// storageContextAsReadOnly sets given context to read-only mode. +func (ic *interopContext) storageContextAsReadOnly(v *vm.VM) error { + stcInterface := v.Estack().Pop().Value() + stc, ok := stcInterface.(*StorageContext) + if !ok { + return fmt.Errorf("%T is not a StorageContext", stcInterface) + } + if !stc.ReadOnly { + stx := &StorageContext{ + ScriptHash: stc.ScriptHash, + ReadOnly: true, + } + stc = stx + } + v.Estack().PushVal(vm.NewInteropItem(stc)) + return nil +} + +// contractDestroy destroys a contract. +func (ic *interopContext) contractDestroy(v *vm.VM) error { + if ic.trigger != 0x10 { + return errors.New("can't destroy contract when not triggered by application") + } + hash := getContextScriptHash(v, 0) + cs := ic.bc.GetContractState(hash) + if cs == nil { + return nil + } + err := deleteContractStateInStore(ic.mem, hash) + if err != nil { + return err + } + if cs.HasStorage() { + siMap, err := ic.bc.GetStorageItems(hash) + if err != nil { + return err + } + for k := range siMap { + _ = deleteStorageItemInStore(ic.mem, hash, []byte(k)) + } + } + return nil +} + +// contractGetStorageContext retrieves StorageContext of a contract. +func (ic *interopContext) contractGetStorageContext(v *vm.VM) error { + csInterface := v.Estack().Pop().Value() + cs, ok := csInterface.(*ContractState) + if !ok { + return fmt.Errorf("%T is not a contract state", cs) + } + if getContractStateFromStore(ic.mem, cs.ScriptHash()) == nil { + return fmt.Errorf("contract was not created in this transaction") + } + stc := &StorageContext{ + ScriptHash: cs.ScriptHash(), + } + v.Estack().PushVal(vm.NewInteropItem(stc)) + return nil +} diff --git a/pkg/core/interops.go b/pkg/core/interops.go new file mode 100644 index 000000000..02ad6ea23 --- /dev/null +++ b/pkg/core/interops.go @@ -0,0 +1,218 @@ +package core + +/* + Interops are designed to run under VM's execute() panic protection, so it's OK + for them to do things like + smth := v.Estack().Pop().Bytes() + even though technically Pop() can return a nil pointer. +*/ + +import ( + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/vm" +) + +type interopContext struct { + bc Blockchainer + trigger byte + block *Block + tx *transaction.Transaction + mem *storage.MemoryStore +} + +func newInteropContext(trigger byte, bc Blockchainer, block *Block, tx *transaction.Transaction) *interopContext { + mem := storage.NewMemoryStore() + return &interopContext{bc, trigger, block, tx, mem} +} + +// All lists are sorted, keep 'em this way, please. + +// getSystemInteropMap returns interop mappings for System namespace. +func (ic *interopContext) getSystemInteropMap() map[string]vm.InteropFuncPrice { + return map[string]vm.InteropFuncPrice{ + "System.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1}, + "System.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1}, + "System.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1}, + "System.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200}, + "System.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100}, + "System.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100}, + "System.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1}, + "System.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 200}, + "System.Blockchain.GetTransactionHeight": {Func: ic.bcGetTransactionHeight, Price: 100}, + "System.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, + "System.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1}, + "System.ExecutionEngine.GetCallingScriptHash": {Func: ic.engineGetCallingScriptHash, Price: 1}, + "System.ExecutionEngine.GetEntryScriptHash": {Func: ic.engineGetEntryScriptHash, Price: 1}, + "System.ExecutionEngine.GetExecutingScriptHash": {Func: ic.engineGetExecutingScriptHash, Price: 1}, + "System.ExecutionEngine.GetScriptContainer": {Func: ic.engineGetScriptContainer, Price: 1}, + "System.Header.GetHash": {Func: ic.headerGetHash, Price: 1}, + "System.Header.GetIndex": {Func: ic.headerGetIndex, Price: 1}, + "System.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1}, + "System.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1}, + "System.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200}, + "System.Runtime.GetTime": {Func: ic.runtimeGetTime, Price: 1}, + "System.Runtime.GetTrigger": {Func: ic.runtimeGetTrigger, Price: 1}, + "System.Runtime.Log": {Func: ic.runtimeLog, Price: 1}, + "System.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1}, + "System.Runtime.Platform": {Func: ic.runtimePlatform, Price: 1}, + "System.Storage.Delete": {Func: ic.storageDelete, Price: 100}, + "System.Storage.Get": {Func: ic.storageGet, Price: 100}, + "System.Storage.GetContext": {Func: ic.storageGetContext, Price: 1}, + "System.Storage.GetReadOnlyContext": {Func: ic.storageGetReadOnlyContext, Price: 1}, + "System.Storage.Put": {Func: ic.storagePut, Price: 0}, // These don't have static price in C# code. + "System.Storage.PutEx": {Func: ic.storagePutEx, Price: 0}, + "System.StorageContext.AsReadOnly": {Func: ic.storageContextAsReadOnly, Price: 1}, + "System.Transaction.GetHash": {Func: ic.txGetHash, Price: 1}, + // "System.Runtime.Deserialize": {Func: ic.runtimeDeserialize, Price: 1}, + // "System.Runtime.Serialize": {Func: ic.runtimeSerialize, Price: 1}, + } +} + +// getSystemInteropMap returns interop mappings for Neo and (legacy) AntShares namespaces. +func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice { + return map[string]vm.InteropFuncPrice{ + "Neo.Account.GetBalance": {Func: ic.accountGetBalance, Price: 1}, + "Neo.Account.GetScriptHash": {Func: ic.accountGetScriptHash, Price: 1}, + "Neo.Account.GetVotes": {Func: ic.accountGetVotes, Price: 1}, + "Neo.Account.IsStandard": {Func: ic.accountIsStandard, Price: 100}, + "Neo.Asset.Create": {Func: ic.assetCreate, Price: 0}, + "Neo.Asset.GetAdmin": {Func: ic.assetGetAdmin, Price: 1}, + "Neo.Asset.GetAmount": {Func: ic.assetGetAmount, Price: 1}, + "Neo.Asset.GetAssetId": {Func: ic.assetGetAssetID, Price: 1}, + "Neo.Asset.GetAssetType": {Func: ic.assetGetAssetType, Price: 1}, + "Neo.Asset.GetAvailable": {Func: ic.assetGetAvailable, Price: 1}, + "Neo.Asset.GetIssuer": {Func: ic.assetGetIssuer, Price: 1}, + "Neo.Asset.GetOwner": {Func: ic.assetGetOwner, Price: 1}, + "Neo.Asset.GetPrecision": {Func: ic.assetGetPrecision, Price: 1}, + "Neo.Asset.Renew": {Func: ic.assetRenew, Price: 0}, + "Neo.Attribute.GetData": {Func: ic.attrGetData, Price: 1}, + "Neo.Attribute.GetUsage": {Func: ic.attrGetUsage, Price: 1}, + "Neo.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1}, + "Neo.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1}, + "Neo.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1}, + "Neo.Blockchain.GetAccount": {Func: ic.bcGetAccount, Price: 100}, + "Neo.Blockchain.GetAsset": {Func: ic.bcGetAsset, Price: 100}, + "Neo.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200}, + "Neo.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100}, + "Neo.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100}, + "Neo.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1}, + "Neo.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 100}, + "Neo.Blockchain.GetTransactionHeight": {Func: ic.bcGetTransactionHeight, Price: 100}, + "Neo.Contract.Create": {Func: ic.contractCreate, Price: 0}, + "Neo.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, + "Neo.Contract.GetScript": {Func: ic.contractGetScript, Price: 1}, + "Neo.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1}, + "Neo.Contract.IsPayable": {Func: ic.contractIsPayable, Price: 1}, + "Neo.Contract.Migrate": {Func: ic.contractMigrate, Price: 0}, + "Neo.Header.GetConsensusData": {Func: ic.headerGetConsensusData, Price: 1}, + "Neo.Header.GetHash": {Func: ic.headerGetHash, Price: 1}, + "Neo.Header.GetIndex": {Func: ic.headerGetIndex, Price: 1}, + "Neo.Header.GetMerkleRoot": {Func: ic.headerGetMerkleRoot, Price: 1}, + "Neo.Header.GetNextConsensus": {Func: ic.headerGetNextConsensus, Price: 1}, + "Neo.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1}, + "Neo.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1}, + "Neo.Header.GetVersion": {Func: ic.headerGetVersion, Price: 1}, + "Neo.Input.GetHash": {Func: ic.inputGetHash, Price: 1}, + "Neo.Input.GetIndex": {Func: ic.inputGetIndex, Price: 1}, + "Neo.Output.GetAssetId": {Func: ic.outputGetAssetID, Price: 1}, + "Neo.Output.GetScriptHash": {Func: ic.outputGetScriptHash, Price: 1}, + "Neo.Output.GetValue": {Func: ic.outputGetValue, Price: 1}, + "Neo.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200}, + "Neo.Runtime.GetTime": {Func: ic.runtimeGetTime, Price: 1}, + "Neo.Runtime.GetTrigger": {Func: ic.runtimeGetTrigger, Price: 1}, + "Neo.Runtime.Log": {Func: ic.runtimeLog, Price: 1}, + "Neo.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1}, + "Neo.Storage.Delete": {Func: ic.storageDelete, Price: 100}, + "Neo.Storage.Get": {Func: ic.storageGet, Price: 100}, + "Neo.Storage.GetContext": {Func: ic.storageGetContext, Price: 1}, + "Neo.Storage.GetReadOnlyContext": {Func: ic.storageGetReadOnlyContext, Price: 1}, + "Neo.Storage.Put": {Func: ic.storagePut, Price: 0}, + "Neo.StorageContext.AsReadOnly": {Func: ic.storageContextAsReadOnly, Price: 1}, + "Neo.Transaction.GetAttributes": {Func: ic.txGetAttributes, Price: 1}, + "Neo.Transaction.GetHash": {Func: ic.txGetHash, Price: 1}, + "Neo.Transaction.GetInputs": {Func: ic.txGetInputs, Price: 1}, + "Neo.Transaction.GetOutputs": {Func: ic.txGetOutputs, Price: 1}, + "Neo.Transaction.GetReferences": {Func: ic.txGetReferences, Price: 200}, + "Neo.Transaction.GetType": {Func: ic.txGetType, Price: 1}, + "Neo.Transaction.GetUnspentCoins": {Func: ic.txGetUnspentCoins, Price: 200}, + "Neo.Transaction.GetWitnesses": {Func: ic.txGetWitnesses, Price: 200}, + // "Neo.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200}, + // "Neo.Enumerator.Concat": {Func: ic.enumeratorConcat, Price: 1}, + // "Neo.Enumerator.Create": {Func: ic.enumeratorCreate, Price: 1}, + // "Neo.Enumerator.Next": {Func: ic.enumeratorNext, Price: 1}, + // "Neo.Enumerator.Value": {Func: ic.enumeratorValue, Price: 1}, + // "Neo.InvocationTransaction.GetScript": {ic.invocationTx_GetScript, 1}, + // "Neo.Iterator.Concat": {Func: ic.iteratorConcat, Price: 1}, + // "Neo.Iterator.Create": {Func: ic.iteratorCreate, Price: 1}, + // "Neo.Iterator.Key": {Func: ic.iteratorKey, Price: 1}, + // "Neo.Iterator.Keys": {Func: ic.iteratorKeys, Price: 1}, + // "Neo.Iterator.Values": {Func: ic.iteratorValues, Price: 1}, + // "Neo.Runtime.Deserialize": {Func: ic.runtimeDeserialize, Price: 1}, + // "Neo.Runtime.Serialize": {Func: ic.runtimeSerialize, Price: 1}, + // "Neo.Storage.Find": {Func: ic.storageFind, Price: 1}, + // "Neo.Witness.GetVerificationScript": {Func: ic.witnessGetVerificationScript, Price: 100}, + + // Aliases. + // "Neo.Iterator.Next": {Func: ic.enumeratorNext, Price: 1}, + // "Neo.Iterator.Value": {Func: ic.enumeratorValue, Price: 1}, + + // Old compatibility APIs. + "AntShares.Account.GetBalance": {Func: ic.accountGetBalance, Price: 1}, + "AntShares.Account.GetScriptHash": {Func: ic.accountGetScriptHash, Price: 1}, + "AntShares.Account.GetVotes": {Func: ic.accountGetVotes, Price: 1}, + "AntShares.Asset.Create": {Func: ic.assetCreate, Price: 0}, + "AntShares.Asset.GetAdmin": {Func: ic.assetGetAdmin, Price: 1}, + "AntShares.Asset.GetAmount": {Func: ic.assetGetAmount, Price: 1}, + "AntShares.Asset.GetAssetId": {Func: ic.assetGetAssetID, Price: 1}, + "AntShares.Asset.GetAssetType": {Func: ic.assetGetAssetType, Price: 1}, + "AntShares.Asset.GetAvailable": {Func: ic.assetGetAvailable, Price: 1}, + "AntShares.Asset.GetIssuer": {Func: ic.assetGetIssuer, Price: 1}, + "AntShares.Asset.GetOwner": {Func: ic.assetGetOwner, Price: 1}, + "AntShares.Asset.GetPrecision": {Func: ic.assetGetPrecision, Price: 1}, + "AntShares.Asset.Renew": {Func: ic.assetRenew, Price: 0}, + "AntShares.Attribute.GetData": {Func: ic.attrGetData, Price: 1}, + "AntShares.Attribute.GetUsage": {Func: ic.attrGetUsage, Price: 1}, + "AntShares.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1}, + "AntShares.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1}, + "AntShares.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1}, + "AntShares.Blockchain.GetAccount": {Func: ic.bcGetAccount, Price: 100}, + "AntShares.Blockchain.GetAsset": {Func: ic.bcGetAsset, Price: 100}, + "AntShares.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200}, + "AntShares.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100}, + "AntShares.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100}, + "AntShares.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1}, + "AntShares.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 100}, + "AntShares.Contract.Create": {Func: ic.contractCreate, Price: 0}, + "AntShares.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, + "AntShares.Contract.GetScript": {Func: ic.contractGetScript, Price: 1}, + "AntShares.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1}, + "AntShares.Contract.Migrate": {Func: ic.contractMigrate, Price: 0}, + "AntShares.Header.GetConsensusData": {Func: ic.headerGetConsensusData, Price: 1}, + "AntShares.Header.GetHash": {Func: ic.headerGetHash, Price: 1}, + "AntShares.Header.GetMerkleRoot": {Func: ic.headerGetMerkleRoot, Price: 1}, + "AntShares.Header.GetNextConsensus": {Func: ic.headerGetNextConsensus, Price: 1}, + "AntShares.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1}, + "AntShares.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1}, + "AntShares.Header.GetVersion": {Func: ic.headerGetVersion, Price: 1}, + "AntShares.Input.GetHash": {Func: ic.inputGetHash, Price: 1}, + "AntShares.Input.GetIndex": {Func: ic.inputGetIndex, Price: 1}, + "AntShares.Output.GetAssetId": {Func: ic.outputGetAssetID, Price: 1}, + "AntShares.Output.GetScriptHash": {Func: ic.outputGetScriptHash, Price: 1}, + "AntShares.Output.GetValue": {Func: ic.outputGetValue, Price: 1}, + "AntShares.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200}, + "AntShares.Runtime.Log": {Func: ic.runtimeLog, Price: 1}, + "AntShares.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1}, + "AntShares.Storage.Delete": {Func: ic.storageDelete, Price: 100}, + "AntShares.Storage.Get": {Func: ic.storageGet, Price: 100}, + "AntShares.Storage.GetContext": {Func: ic.storageGetContext, Price: 1}, + "AntShares.Storage.Put": {Func: ic.storagePut, Price: 0}, + "AntShares.Transaction.GetAttributes": {Func: ic.txGetAttributes, Price: 1}, + "AntShares.Transaction.GetHash": {Func: ic.txGetHash, Price: 1}, + "AntShares.Transaction.GetInputs": {Func: ic.txGetInputs, Price: 1}, + "AntShares.Transaction.GetOutputs": {Func: ic.txGetOutputs, Price: 1}, + "AntShares.Transaction.GetReferences": {Func: ic.txGetReferences, Price: 200}, + "AntShares.Transaction.GetType": {Func: ic.txGetType, Price: 1}, + // "AntShares.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200}, + } +} diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index f42cb597c..dbb55f607 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -109,7 +109,7 @@ func (chain testChain) IsLowPriority(*transaction.Transaction) bool { panic("TODO") } -func (chain testChain) Verify(*transaction.Transaction) error { +func (chain testChain) VerifyTx(*transaction.Transaction, *core.Block) error { panic("TODO") } diff --git a/pkg/network/server.go b/pkg/network/server.go index a1caa2fa5..039c8a01d 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -399,7 +399,7 @@ func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason { if s.chain.HasTransaction(t.Hash()) { return RelayAlreadyExists } - if err := s.chain.Verify(t); err != nil { + if err := s.chain.VerifyTx(t, nil); err != nil { return RelayInvalid } // TODO: Implement Plugin.CheckPolicy? From 56dcff2894bbe3ed8bb5663134dcaad209b02a5b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 17:02:59 +0300 Subject: [PATCH 27/31] core: invoke contracts for Invocation TXes They can have some side-effects that future invocations rely on. --- pkg/core/blockchain.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 94e74c19d..671b46871 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -410,6 +410,32 @@ func (bc *Blockchain) storeBlock(block *Block) error { contracts[contract.ScriptHash()] = contract case *transaction.InvocationTX: + vm := vm.New(vm.ModeMute) + vm.SetCheckedHash(tx.VerificationHash().Bytes()) + vm.SetScriptGetter(func(hash util.Uint160) []byte { + cs := bc.GetContractState(hash) + if cs == nil { + return nil + } + + return cs.Script + }) + systemInterop := newInteropContext(0x10, bc, block, tx) + vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) + vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) + vm.LoadScript(t.Script) + vm.Run() + if !vm.HasFailed() { + _, err := systemInterop.mem.Persist(bc.memStore) + if err != nil { + return errors.Wrap(err, "failed to persist invocation results") + } + } else { + log.WithFields(log.Fields{ + "tx": tx.Hash().ReverseString(), + "block": block.Index, + }).Warn("contract invocation failed") + } } } From aec6a5f029c10e2868bfd95d32e799e3e4f9ed33 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 17:46:47 +0300 Subject: [PATCH 28/31] core/config: make block and transaction verification configurable Enable transaction verification for privnets and tests, testnet can't successfuly verify block number 316711 with it enabled and mainnet stops at 105829. --- config/config.go | 4 ++++ config/protocol.mainnet.yml | 2 ++ config/protocol.privnet.docker.four.yml | 2 ++ config/protocol.privnet.docker.one.yml | 2 ++ config/protocol.privnet.docker.three.yml | 2 ++ config/protocol.privnet.docker.two.yml | 2 ++ config/protocol.privnet.yml | 2 ++ config/protocol.testnet.yml | 2 ++ config/protocol.unit_testnet.yml | 2 ++ pkg/core/blockchain.go | 16 +++++++--------- 10 files changed, 27 insertions(+), 9 deletions(-) diff --git a/config/config.go b/config/config.go index 16673f5b7..d63c9f1d6 100644 --- a/config/config.go +++ b/config/config.go @@ -49,6 +49,10 @@ type ( StandbyValidators []string `yaml:"StandbyValidators"` SeedList []string `yaml:"SeedList"` SystemFee SystemFee `yaml:"SystemFee"` + // Whether to verify received blocks. + VerifyBlocks bool `yaml:"VerifyBlocks"` + // Whether to verify transactions in received blocks. + VerifyTransactions bool `yaml:"VerifyTransactions"` } // SystemFee fees related to system. diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index 6292ffe58..87805be90 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -27,6 +27,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: false ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.privnet.docker.four.yml b/config/protocol.privnet.docker.four.yml index d530183ff..b18ac83cd 100644 --- a/config/protocol.privnet.docker.four.yml +++ b/config/protocol.privnet.docker.four.yml @@ -15,6 +15,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: true ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.privnet.docker.one.yml b/config/protocol.privnet.docker.one.yml index 96e88f07a..2637c6635 100644 --- a/config/protocol.privnet.docker.one.yml +++ b/config/protocol.privnet.docker.one.yml @@ -12,6 +12,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: true ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.privnet.docker.three.yml b/config/protocol.privnet.docker.three.yml index 82206637c..c2f3e237b 100644 --- a/config/protocol.privnet.docker.three.yml +++ b/config/protocol.privnet.docker.three.yml @@ -12,6 +12,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: true ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.privnet.docker.two.yml b/config/protocol.privnet.docker.two.yml index 422e452ed..3f8a2b929 100644 --- a/config/protocol.privnet.docker.two.yml +++ b/config/protocol.privnet.docker.two.yml @@ -12,6 +12,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: true ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.privnet.yml b/config/protocol.privnet.yml index 9e28b12d6..2a31395fc 100644 --- a/config/protocol.privnet.yml +++ b/config/protocol.privnet.yml @@ -18,6 +18,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: true ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index 13b33a90d..7e3f2e332 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -27,6 +27,8 @@ ProtocolConfiguration: IssueTransaction: 5 PublishTransaction: 5 RegisterTransaction: 100 + VerifyBlocks: true + VerifyTransactions: false ApplicationConfiguration: DBConfiguration: diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index eb3490e85..1bf522cb3 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -17,6 +17,8 @@ ProtocolConfiguration: IssueTransaction: 500 PublishTransaction: 500 RegisterTransaction: 10000 + VerifyBlocks: true + VerifyTransactions: true ApplicationConfiguration: DBConfiguration: diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 671b46871..d286e425e 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -68,9 +68,6 @@ type Blockchain struct { headersOp chan headersOpFunc headersOpDone chan struct{} - // Whether we will verify received blocks. - verifyBlocks bool - memPool MemPool } @@ -85,7 +82,6 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration) (*Blockcha memStore: storage.NewMemoryStore(), headersOp: make(chan headersOpFunc), headersOpDone: make(chan struct{}), - verifyBlocks: false, memPool: NewMemPool(50000), } @@ -209,15 +205,17 @@ func (bc *Blockchain) AddBlock(block *Block) error { if expectedHeight != block.Index { return fmt.Errorf("expected block %d, but passed block %d", expectedHeight, block.Index) } - if bc.verifyBlocks { + if bc.config.VerifyBlocks { err := block.Verify(false) if err != nil { return fmt.Errorf("block %s is invalid: %s", block.Hash().ReverseString(), err) } - for _, tx := range block.Transactions { - err := bc.VerifyTx(tx, block) - if err != nil { - return fmt.Errorf("transaction %s failed to verify: %s", tx.Hash().ReverseString(), err) + if bc.config.VerifyTransactions { + for _, tx := range block.Transactions { + err := bc.VerifyTx(tx, block) + if err != nil { + return fmt.Errorf("transaction %s failed to verify: %s", tx.Hash().ReverseString(), err) + } } } } From 03c20f187604595d36a32df1c7f4acfa7232caec Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 11 Oct 2019 18:21:34 +0300 Subject: [PATCH 29/31] core: make ContractState receiver names consistent Linter isn't happy with our recent changes: pkg/core/contract_state.go:109:1: receiver name cs should be consistent with previous receiver name a for ContractState pkg/core/contract_state.go:114:1: receiver name cs should be consistent with previous receiver name a for ContractState pkg/core/contract_state.go:119:1: receiver name cs should be consistent with previous receiver name a for ContractState But actually `a` here most probably is a copy-paste from AssetState methods, so fit the old code to match the new one. --- pkg/core/contract_state.go | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/pkg/core/contract_state.go b/pkg/core/contract_state.go index cd3c8aea9..1e0a582e8 100644 --- a/pkg/core/contract_state.go +++ b/pkg/core/contract_state.go @@ -42,37 +42,37 @@ func (a Contracts) commit(b storage.Batch) error { } // DecodeBinary implements Serializable interface. -func (a *ContractState) DecodeBinary(br *io.BinReader) { - a.Script = br.ReadBytes() +func (cs *ContractState) DecodeBinary(br *io.BinReader) { + cs.Script = br.ReadBytes() paramBytes := br.ReadBytes() - a.ParamList = make([]smartcontract.ParamType, len(paramBytes)) + cs.ParamList = make([]smartcontract.ParamType, len(paramBytes)) for k := range paramBytes { - a.ParamList[k] = smartcontract.ParamType(paramBytes[k]) + cs.ParamList[k] = smartcontract.ParamType(paramBytes[k]) } - br.ReadLE(&a.ReturnType) - br.ReadLE(&a.Properties) - a.Name = br.ReadString() - a.CodeVersion = br.ReadString() - a.Author = br.ReadString() - a.Email = br.ReadString() - a.Description = br.ReadString() - a.createHash() + br.ReadLE(&cs.ReturnType) + br.ReadLE(&cs.Properties) + cs.Name = br.ReadString() + cs.CodeVersion = br.ReadString() + cs.Author = br.ReadString() + cs.Email = br.ReadString() + cs.Description = br.ReadString() + cs.createHash() } // EncodeBinary implements Serializable interface. -func (a *ContractState) EncodeBinary(bw *io.BinWriter) { - bw.WriteBytes(a.Script) - bw.WriteVarUint(uint64(len(a.ParamList))) - for k := range a.ParamList { - bw.WriteLE(a.ParamList[k]) +func (cs *ContractState) EncodeBinary(bw *io.BinWriter) { + bw.WriteBytes(cs.Script) + bw.WriteVarUint(uint64(len(cs.ParamList))) + for k := range cs.ParamList { + bw.WriteLE(cs.ParamList[k]) } - bw.WriteLE(a.ReturnType) - bw.WriteLE(a.Properties) - bw.WriteString(a.Name) - bw.WriteString(a.CodeVersion) - bw.WriteString(a.Author) - bw.WriteString(a.Email) - bw.WriteString(a.Description) + bw.WriteLE(cs.ReturnType) + bw.WriteLE(cs.Properties) + bw.WriteString(cs.Name) + bw.WriteString(cs.CodeVersion) + bw.WriteString(cs.Author) + bw.WriteString(cs.Email) + bw.WriteString(cs.Description) } // putContractStateIntoStore puts given contract state into the given store. @@ -93,16 +93,16 @@ func deleteContractStateInStore(s storage.Store, hash util.Uint160) error { } // ScriptHash returns a contract script hash. -func (a *ContractState) ScriptHash() util.Uint160 { - if a.scriptHash.Equals(util.Uint160{}) { - a.createHash() +func (cs *ContractState) ScriptHash() util.Uint160 { + if cs.scriptHash.Equals(util.Uint160{}) { + cs.createHash() } - return a.scriptHash + return cs.scriptHash } // createHash creates contract script hash. -func (a *ContractState) createHash() { - a.scriptHash = hash.Hash160(a.Script) +func (cs *ContractState) createHash() { + cs.scriptHash = hash.Hash160(cs.Script) } // HasStorage checks whether the contract has storage property set. From a6610ba08207d83c093bf1b5c42011e80eccc0d7 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 15 Oct 2019 12:52:10 +0300 Subject: [PATCH 30/31] core: verify blocks, fix #12 This adds the following verifications: * merkleroot check * index check * timestamp check * witnesses verification VerifyWitnesses is also renamed to verifyTxWitnesses here to not confuse it with verifyBlockWitnesse and to hide it from external access (no users at the moment). --- pkg/core/block.go | 26 +++--- pkg/core/block_base.go | 35 ++++++-- pkg/core/block_test.go | 48 +++++++++-- pkg/core/blockchain.go | 136 +++++++++++++++++++----------- pkg/core/blockchain_test.go | 19 +---- pkg/core/helper_test.go | 86 +++++++++++++++---- pkg/rpc/server_helper_test.go | 63 ++++---------- pkg/rpc/testdata/50testblocks.acc | Bin 0 -> 25954 bytes 8 files changed, 260 insertions(+), 153 deletions(-) create mode 100644 pkg/rpc/testdata/50testblocks.acc diff --git a/pkg/core/block.go b/pkg/core/block.go index 23bc73c40..d15c4beee 100644 --- a/pkg/core/block.go +++ b/pkg/core/block.go @@ -9,7 +9,6 @@ import ( "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" "github.com/Workiva/go-datastructures/queue" - log "github.com/sirupsen/logrus" ) // Block represents one block in the chain. @@ -31,14 +30,18 @@ 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 { +func merkleTreeFromTransactions(txes []*transaction.Transaction) (*crypto.MerkleTree, error) { + hashes := make([]util.Uint256, len(txes)) + for i, tx := range txes { hashes[i] = tx.Hash() } - merkle, err := crypto.NewMerkleTree(hashes) + return crypto.NewMerkleTree(hashes) +} + +// rebuildMerkleRoot rebuild the merkleroot of the block. +func (b *Block) rebuildMerkleRoot() error { + merkle, err := merkleTreeFromTransactions(b.Transactions) if err != nil { return err } @@ -48,7 +51,7 @@ func (b *Block) rebuildMerkleRoot() error { } // Verify the integrity of the block. -func (b *Block) Verify(full bool) error { +func (b *Block) Verify() error { // There has to be some transaction inside. if len(b.Transactions) == 0 { return errors.New("no transactions") @@ -63,9 +66,12 @@ func (b *Block) Verify(full bool) error { return fmt.Errorf("miner transaction %s is not the first one", tx.Hash().ReverseString()) } } - // TODO: When full is true, do a full verification. - if full { - log.Warn("full verification of blocks is not yet implemented") + merkle, err := merkleTreeFromTransactions(b.Transactions) + if err != nil { + return err + } + if !b.MerkleRoot.Equals(merkle.Root()) { + return errors.New("MerkleRoot mismatch") } return nil } diff --git a/pkg/core/block_base.go b/pkg/core/block_base.go index 6b77e1b5b..97077b690 100644 --- a/pkg/core/block_base.go +++ b/pkg/core/block_base.go @@ -40,8 +40,11 @@ type BlockBase struct { // Script used to validate the block Script *transaction.Witness `json:"script"` - // hash of this block, created when binary encoded. + // Hash of this block, created when binary encoded (double SHA256). hash util.Uint256 + + // Hash of the block used to verify it (single SHA256). + verificationHash util.Uint256 } // Verify verifies the integrity of the BlockBase. @@ -58,6 +61,16 @@ func (b *BlockBase) Hash() util.Uint256 { return b.hash } +// VerificationHash returns the hash of the block used to verify it. +func (b *BlockBase) VerificationHash() util.Uint256 { + if b.verificationHash.Equals(util.Uint256{}) { + if b.createHash() != nil { + panic("failed to compute hash!") + } + } + return b.verificationHash +} + // DecodeBinary implements Serializable interface. func (b *BlockBase) DecodeBinary(br *io.BinReader) { b.decodeHashableFields(br) @@ -80,6 +93,16 @@ func (b *BlockBase) EncodeBinary(bw *io.BinWriter) { b.Script.EncodeBinary(bw) } +// getHashableData returns serialized hashable data of the block. +func (b *BlockBase) getHashableData() ([]byte, error) { + buf := io.NewBufBinWriter() + b.encodeHashableFields(buf.BinWriter) + if buf.Err != nil { + return nil, buf.Err + } + return buf.Bytes(), nil +} + // createHash creates the hash of the block. // When calculating the hash value of the block, instead of calculating the entire block, // only first seven fields in the block head will be calculated, which are @@ -87,12 +110,12 @@ func (b *BlockBase) EncodeBinary(bw *io.BinWriter) { // 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() error { - buf := io.NewBufBinWriter() - b.encodeHashableFields(buf.BinWriter) - if buf.Err != nil { - return buf.Err + bb, err := b.getHashableData() + if err != nil { + return err } - b.hash = hash.DoubleSha256(buf.Bytes()) + b.hash = hash.DoubleSha256(bb) + b.verificationHash = hash.Sha256(bb) return nil } diff --git a/pkg/core/block_test.go b/pkg/core/block_test.go index bb7016826..1c2f5e790 100644 --- a/pkg/core/block_test.go +++ b/pkg/core/block_test.go @@ -6,6 +6,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/io" "github.com/stretchr/testify/assert" ) @@ -76,30 +77,59 @@ func TestTrimmedBlock(t *testing.T) { } } +func newDumbBlock() *Block { + return &Block{ + BlockBase: BlockBase{ + Version: 0, + PrevHash: hash.Sha256([]byte("a")), + MerkleRoot: hash.Sha256([]byte("b")), + Timestamp: uint32(100500), + Index: 1, + ConsensusData: 1111, + NextConsensus: hash.Hash160([]byte("a")), + Script: &transaction.Witness{ + VerificationScript: []byte{0x51}, // PUSH1 + InvocationScript: []byte{0x61}, // NOP + }, + }, + Transactions: []*transaction.Transaction{ + {Type: transaction.MinerType}, + {Type: transaction.IssueType}, + }, + } +} + func TestHashBlockEqualsHashHeader(t *testing.T) { - block := newBlock(0) + block := newDumbBlock() + assert.Equal(t, block.Hash(), block.Header().Hash()) } func TestBlockVerify(t *testing.T) { - block := newBlock( - 0, - newMinerTX(), - newIssueTX(), - ) - assert.Nil(t, block.Verify(false)) + block := newDumbBlock() + assert.NotNil(t, block.Verify()) + assert.Nil(t, block.rebuildMerkleRoot()) + assert.Nil(t, block.Verify()) block.Transactions = []*transaction.Transaction{ {Type: transaction.IssueType}, {Type: transaction.MinerType}, } - assert.NotNil(t, block.Verify(false)) + assert.NoError(t, block.rebuildMerkleRoot()) + assert.NotNil(t, block.Verify()) block.Transactions = []*transaction.Transaction{ {Type: transaction.MinerType}, {Type: transaction.MinerType}, } - assert.NotNil(t, block.Verify(false)) + assert.NoError(t, block.rebuildMerkleRoot()) + assert.NotNil(t, block.Verify()) + block.Transactions = []*transaction.Transaction{ + {Type: transaction.MinerType}, + {Type: transaction.IssueType}, + {Type: transaction.IssueType}, + } + assert.NotNil(t, block.Verify()) } func TestBinBlockDecodeEncode(t *testing.T) { diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d286e425e..8b7145199 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -206,7 +206,10 @@ func (bc *Blockchain) AddBlock(block *Block) error { return fmt.Errorf("expected block %d, but passed block %d", expectedHeight, block.Index) } if bc.config.VerifyBlocks { - err := block.Verify(false) + err := block.Verify() + if err == nil { + err = bc.VerifyBlock(block) + } if err != nil { return fmt.Errorf("block %s is invalid: %s", block.Hash().ReverseString(), err) } @@ -840,6 +843,21 @@ func (bc *Blockchain) GetMemPool() MemPool { return bc.memPool } +// VerifyBlock verifies block against its current state. +func (bc *Blockchain) VerifyBlock(block *Block) error { + prevHeader, err := bc.GetHeader(block.PrevHash) + if err != nil { + return errors.Wrap(err, "unable to get previous header") + } + if prevHeader.Index+1 != block.Index { + return errors.New("previous header index doesn't match") + } + if prevHeader.Timestamp >= block.Timestamp { + return errors.New("block is not newer than the previous one") + } + return bc.verifyBlockWitnesses(block, prevHeader) +} + // VerifyTx verifies whether a transaction is bonafide or not. Block parameter // is used for easy interop access and can be omitted for transactions that are // not yet added into any block. @@ -870,7 +888,7 @@ func (bc *Blockchain) VerifyTx(t *transaction.Transaction, block *Block) error { } } - return bc.VerifyWitnesses(t, block) + return bc.verifyTxWitnesses(t, block) } func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool { @@ -1094,14 +1112,63 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([ } -// VerifyWitnesses verify the scripts (witnesses) that come with a given +// verifyHashAgainstScript verifies given hash against the given witness. +func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error { + verification := witness.VerificationScript + + if len(verification) == 0 { + bb := new(bytes.Buffer) + err := vm.EmitAppCall(bb, hash, false) + if err != nil { + return err + } + verification = bb.Bytes() + } else { + if h := witness.ScriptHash(); hash != h { + return errors.New("witness hash mismatch") + } + } + + vm := vm.New(vm.ModeMute) + vm.SetCheckedHash(checkedHash.Bytes()) + vm.SetScriptGetter(func(hash util.Uint160) []byte { + cs := bc.GetContractState(hash) + if cs == nil { + return nil + } + return cs.Script + }) + vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap()) + vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) + vm.LoadScript(verification) + vm.LoadScript(witness.InvocationScript) + vm.Run() + if vm.HasFailed() { + return errors.Errorf("vm failed to execute the script") + } + resEl := vm.Estack().Pop() + if resEl != nil { + res, err := resEl.TryBool() + if err != nil { + return err + } + if !res { + return errors.Errorf("signature check failed") + } + } else { + return errors.Errorf("no result returned from the script") + } + return nil +} + +// verifyTxWitnesses verify the scripts (witnesses) that come with a given // transaction. It can reorder them by ScriptHash, because that's required to // match a slice of script hashes from the Blockchain. Block parameter // is used for easy interop access and can be omitted for transactions that are // not yet added into any block. // Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). // Unfortunately the IVerifiable interface could not be implemented because we can't move the References method in blockchain.go to the transaction.go file -func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction, block *Block) error { +func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *Block) error { hashes, err := bc.GetScriptHashesForVerifying(t) if err != nil { return err @@ -1113,57 +1180,30 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction, block *Block) } sort.Slice(hashes, func(i, j int) bool { return hashes[i].Less(hashes[j]) }) sort.Slice(witnesses, func(i, j int) bool { return witnesses[i].ScriptHash().Less(witnesses[j].ScriptHash()) }) + interopCtx := newInteropContext(0, bc, block, t) for i := 0; i < len(hashes); i++ { - verification := witnesses[i].VerificationScript - - if len(verification) == 0 { - bb := new(bytes.Buffer) - err = vm.EmitAppCall(bb, hashes[i], false) - if err != nil { - return err - } - verification = bb.Bytes() - } else { - if h := witnesses[i].ScriptHash(); hashes[i] != h { - return errors.Errorf("hash mismatch for script #%d", i) - } - } - - vm := vm.New(vm.ModeMute) - vm.SetCheckedHash(t.VerificationHash().Bytes()) - vm.SetScriptGetter(func(hash util.Uint160) []byte { - cs := bc.GetContractState(hash) - if cs == nil { - return nil - } - return cs.Script - }) - systemInterop := newInteropContext(0, bc, block, t) - vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap()) - vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap()) - vm.LoadScript(verification) - vm.LoadScript(witnesses[i].InvocationScript) - vm.Run() - if vm.HasFailed() { - return errors.Errorf("vm failed to execute the script") - } - resEl := vm.Estack().Pop() - if resEl != nil { - res, err := resEl.TryBool() - if err != nil { - return err - } - if !res { - return errors.Errorf("signature check failed") - } - } else { - return errors.Errorf("no result returned from the script") + err := bc.verifyHashAgainstScript(hashes[i], witnesses[i], t.VerificationHash(), interopCtx) + if err != nil { + numStr := fmt.Sprintf("witness #%d", i) + return errors.Wrap(err, numStr) } } return nil } +// verifyBlockWitnesses is a block-specific implementation of VerifyWitnesses logic. +func (bc *Blockchain) verifyBlockWitnesses(block *Block, prevHeader *Header) error { + var hash util.Uint160 + if prevHeader == nil && block.PrevHash.Equals(util.Uint256{}) { + hash = block.Script.ScriptHash() + } else { + hash = prevHeader.NextConsensus + } + interopCtx := newInteropContext(0, bc, nil, nil) + return bc.verifyHashAgainstScript(hash, block.Script, block.VerificationHash(), interopCtx) +} + func hashAndIndexToBytes(h util.Uint256, index uint32) []byte { buf := io.NewBufBinWriter() buf.WriteLE(h.BytesReverse()) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 27fc544e5..bf88230e3 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/io" "github.com/stretchr/testify/assert" @@ -137,8 +136,9 @@ func TestGetTransaction(t *testing.T) { block := getDecodedBlock(t, 2) bc := newTestChain(t) - assert.Nil(t, bc.AddBlock(b1)) - assert.Nil(t, bc.AddBlock(block)) + // These are from some kind of different chain, so can't be added via AddBlock(). + assert.Nil(t, bc.storeBlock(b1)) + assert.Nil(t, bc.storeBlock(block)) // Test unpersisted and persisted access for j := 0; j < 2; j++ { @@ -154,16 +154,3 @@ func TestGetTransaction(t *testing.T) { assert.NoError(t, bc.persist(context.Background())) } } - -func newTestChain(t *testing.T) *Blockchain { - cfg, err := config.Load("../../config", config.ModeUnitTestNet) - if err != nil { - t.Fatal(err) - } - chain, err := NewBlockchain(storage.NewMemoryStore(), cfg.ProtocolConfiguration) - if err != nil { - t.Fatal(err) - } - go chain.Run(context.Background()) - return chain -} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 205ae8bbc..112057f32 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -1,6 +1,7 @@ package core import ( + "context" "encoding/hex" "encoding/json" "fmt" @@ -8,32 +9,90 @@ import ( "testing" "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/hash" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/require" ) +var newBlockPrevHash util.Uint256 +var unitTestNetCfg config.Config + +var privNetKeys = []string{ + "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY", + "KzfPUYDC9n2yf4fK5ro4C8KMcdeXtFuEnStycbZgX3GomiUsvX6W", + "KzgWE3u3EDp13XPXXuTKZxeJ3Gi8Bsm8f9ijY3ZsCKKRvZUo1Cdn", + "L2oEXKRAAMiPEZukwR5ho2S6SMeQLhcK9mF71ZnF7GvT8dU4Kkgz", +} + +// newTestChain should be called before newBlock invocation to properly setup +// global state. +func newTestChain(t *testing.T) *Blockchain { + var err error + unitTestNetCfg, err = config.Load("../../config", config.ModeUnitTestNet) + if err != nil { + t.Fatal(err) + } + chain, err := NewBlockchain(storage.NewMemoryStore(), unitTestNetCfg.ProtocolConfiguration) + if err != nil { + t.Fatal(err) + } + go chain.Run(context.Background()) + zeroHash, err := chain.GetHeader(chain.GetHeaderHash(0)) + require.Nil(t, err) + newBlockPrevHash = zeroHash.Hash() + return chain +} + func newBlock(index uint32, txs ...*transaction.Transaction) *Block { + validators, _ := getValidators(unitTestNetCfg.ProtocolConfiguration) + vlen := len(validators) + valScript, _ := smartcontract.CreateMultiSigRedeemScript( + vlen-(vlen-1)/3, + validators, + ) + witness := &transaction.Witness{ + VerificationScript: valScript, + } b := &Block{ BlockBase: BlockBase{ Version: 0, - PrevHash: hash.Sha256([]byte("a")), - MerkleRoot: hash.Sha256([]byte("b")), - Timestamp: uint32(time.Now().UTC().Unix()), + PrevHash: newBlockPrevHash, + Timestamp: uint32(time.Now().UTC().Unix()) + index, Index: index, ConsensusData: 1111, - NextConsensus: util.Uint160{}, - Script: &transaction.Witness{ - VerificationScript: []byte{0x0}, - InvocationScript: []byte{0x1}, - }, + NextConsensus: witness.ScriptHash(), + Script: witness, }, Transactions: txs, } - + _ = b.rebuildMerkleRoot() b.createHash() + newBlockPrevHash = b.Hash() + invScript := make([]byte, 0) + for _, wif := range privNetKeys { + pKey, err := keys.NewPrivateKeyFromWIF(wif) + if err != nil { + panic(err) + } + b, err := b.getHashableData() + if err != nil { + panic(err) + } + sig, err := pKey.Sign(b) + if err != nil || len(sig) != 64 { + panic(err) + } + // 0x40 is PUSHBYTES64 + invScript = append(invScript, 0x40) + invScript = append(invScript, sig...) + } + b.Script.InvocationScript = invScript return b } @@ -52,13 +111,6 @@ func newMinerTX() *transaction.Transaction { } } -func newIssueTX() *transaction.Transaction { - return &transaction.Transaction{ - Type: transaction.IssueType, - Data: &transaction.IssueTX{}, - } -} - func getDecodedBlock(t *testing.T, i int) *Block { data, err := getBlockData(i) if err != nil { diff --git a/pkg/rpc/server_helper_test.go b/pkg/rpc/server_helper_test.go index bbb7f8b34..603564d78 100644 --- a/pkg/rpc/server_helper_test.go +++ b/pkg/rpc/server_helper_test.go @@ -3,18 +3,16 @@ package rpc import ( "context" "net/http" + "os" "testing" - "time" "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/core/transaction" - "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/rpc/result" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" - "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -142,6 +140,8 @@ type GetAccountStateResponse struct { } func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Blockchain, http.HandlerFunc) { + var nBlocks uint32 + net := config.ModeUnitTestNet configPath := "../../config" cfg, err := config.Load(configPath, net) @@ -152,7 +152,18 @@ func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Block require.NoError(t, err, "could not create chain") go chain.Run(ctx) - initBlocks(t, chain) + + f, err := os.Open("testdata/50testblocks.acc") + require.Nil(t, err) + br := io.NewBinReaderFromIO(f) + br.ReadLE(&nBlocks) + require.Nil(t, br.Err) + for i := 0; i < int(nBlocks); i++ { + block := &core.Block{} + block.DecodeBinary(br) + require.Nil(t, br.Err) + require.NoError(t, chain.AddBlock(block)) + } serverConfig := network.NewServerConfig(cfg) server := network.NewServer(serverConfig, chain) @@ -161,45 +172,3 @@ func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Block return chain, handler } - -func initBlocks(t *testing.T, chain *core.Blockchain) { - blocks := makeBlocks(10) - for i := 0; i < len(blocks); i++ { - require.NoError(t, chain.AddBlock(blocks[i])) - } -} - -func makeBlocks(n int) []*core.Block { - blocks := make([]*core.Block, n) - for i := 0; i < n; i++ { - blocks[i] = newBlock(uint32(i+1), newMinerTX()) - } - return blocks -} - -func newMinerTX() *transaction.Transaction { - return &transaction.Transaction{ - Type: transaction.MinerType, - Data: &transaction.MinerTX{}, - } -} - -func newBlock(index uint32, txs ...*transaction.Transaction) *core.Block { - b := &core.Block{ - BlockBase: core.BlockBase{ - Version: 0, - PrevHash: hash.Sha256([]byte("a")), - MerkleRoot: hash.Sha256([]byte("b")), - Timestamp: uint32(time.Now().UTC().Unix()), - Index: index, - ConsensusData: 1111, - NextConsensus: util.Uint160{}, - Script: &transaction.Witness{ - VerificationScript: []byte{0x0}, - InvocationScript: []byte{0x1}, - }, - }, - Transactions: txs, - } - return b -} diff --git a/pkg/rpc/testdata/50testblocks.acc b/pkg/rpc/testdata/50testblocks.acc new file mode 100644 index 0000000000000000000000000000000000000000..91825648d4a58ecebe67c74cddd32342f6c6c091 GIT binary patch literal 25954 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%0DTGrc%qHNt6yn}VP2N1jCQ2r}v!>uoL(dgG6^ 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 Date: Tue, 15 Oct 2019 18:55:32 +0300 Subject: [PATCH 31/31] core: fix potential data race in logging code Spotted in CircleCI build with Go 1.12: WARNING: DATA RACE Write at 0x00c00011095c by goroutine 88: sync/atomic.SwapInt32() /usr/local/go/src/runtime/race_amd64.s:249 +0xb github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).persist() /go/src/github.com/CityOfZion/neo-go/pkg/core/blockchain.go:483 +0x172 github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).Run.func2() /go/src/github.com/CityOfZion/neo-go/pkg/core/blockchain.go:191 +0x50 Previous read at 0x00c00011095c by goroutine 64: github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).persist() /go/src/github.com/CityOfZion/neo-go/pkg/core/blockchain.go:492 +0x4be github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).Run.func2() /go/src/github.com/CityOfZion/neo-go/pkg/core/blockchain.go:191 +0x50 Goroutine 88 (running) created at: github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).Run() /go/src/github.com/CityOfZion/neo-go/pkg/core/blockchain.go:190 +0x264 Goroutine 64 (finished) created at: github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).Run() /go/src/github.com/CityOfZion/neo-go/pkg/core/blockchain.go:190 +0x264 --- pkg/core/blockchain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 8b7145199..50abcd962 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -489,7 +489,7 @@ func (bc *Blockchain) persist(ctx context.Context) error { "persistedKeys": persisted, "headerHeight": bc.HeaderHeight(), "blockHeight": bc.BlockHeight(), - "persistedHeight": bc.persistedHeight, + "persistedHeight": atomic.LoadUint32(&bc.persistedHeight), "took": time.Since(start), }).Info("blockchain persist completed") }