diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d0d4d5574..f9e930737 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -457,7 +457,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { cache := newCachedDao(bc.dao.store) fee := bc.getSystemFeeAmount(block.PrevHash) for _, tx := range block.Transactions { - fee += uint32(bc.SystemFee(tx).Int64Value()) + fee += uint32(bc.SystemFee(tx).IntegralValue()) } if err := cache.StoreAsBlock(block, fee); err != nil { return err @@ -1211,6 +1211,12 @@ func (bc *Blockchain) NetworkFee(t *transaction.Transaction) util.Fixed8 { // SystemFee returns system fee. func (bc *Blockchain) SystemFee(t *transaction.Transaction) util.Fixed8 { + if t.Type == transaction.InvocationType { + inv := t.Data.(*transaction.InvocationTX) + if inv.Version >= 1 { + return inv.Gas + } + } return bc.GetConfig().SystemFee.TryGetValue(t.Type) } @@ -1285,7 +1291,8 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e } } - if t.Type == transaction.ClaimType { + switch t.Type { + case transaction.ClaimType: claim := t.Data.(*transaction.ClaimTX) if transaction.HaveDuplicateInputs(claim.Claims) { return errors.New("duplicate claims") @@ -1296,6 +1303,11 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e if err := bc.verifyClaims(t); err != nil { return err } + case transaction.InvocationType: + inv := t.Data.(*transaction.InvocationTX) + if inv.Gas.FractionalValue() != 0 { + return errors.New("invocation gas can only be integer") + } } return bc.verifyTxWitnesses(t, block) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index eb171e599..fc82226cb 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -93,8 +93,9 @@ func newBlock(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256, func (bc *Blockchain) genBlocks(n int) ([]*block.Block, error) { blocks := make([]*block.Block, n) lastHash := bc.topBlock.Load().(*block.Block).Hash() + lastIndex := bc.topBlock.Load().(*block.Block).Index for i := 0; i < n; i++ { - blocks[i] = newBlock(bc.config, uint32(i)+1, lastHash, newMinerTX()) + blocks[i] = newBlock(bc.config, uint32(i)+lastIndex+1, lastHash, newMinerTX()) if err := bc.AddBlock(blocks[i]); err != nil { return blocks, err } @@ -165,21 +166,117 @@ func getInvocationScript(data []byte, priv *keys.PrivateKey) []byte { } // This function generates "../rpc/testdata/testblocks.acc" file which contains data -// for RPC unit tests. +// for RPC unit tests. It also is a nice integration test. // To generate new "../rpc/testdata/testblocks.acc", follow the steps: -// 1. Rename the function -// 2. Add specific test-case into "neo-go/pkg/core/blockchain_test.go" -// 3. Run tests with `$ make test` -func _(t *testing.T) { +// 1. Set saveChain down below to true +// 2. Run tests with `$ make test` +func TestCreateBasicChain(t *testing.T) { + const saveChain = false const prefix = "../rpc/server/testdata/" + // To make enough GAS. + const numOfEmptyBlocks = 200 + var neoAmount = util.Fixed8FromInt64(99999000) + var neoRemainder = util.Fixed8FromInt64(100000000) - neoAmount bc := newTestChain(t) - n := 50 - _, err := bc.genBlocks(n) + + // Move almost all NEO to one simple account. + txMoveNeo := transaction.NewContractTX() + h, err := util.Uint256DecodeStringBE("6da730b566db183bfceb863b780cd92dee2b497e5a023c322c1eaca81cf9ad7a") + require.NoError(t, err) + txMoveNeo.AddInput(&transaction.Input{ + PrevHash: h, + PrevIndex: 0, + }) + + // multisig address which possess all NEO + scriptHash, err := util.Uint160DecodeStringBE("be48d3a3f5d10013ab9ffee489706078714f1ea2") + require.NoError(t, err) + priv0, err := keys.NewPrivateKeyFromWIF(privNetKeys[0]) + require.NoError(t, err) + txMoveNeo.AddOutput(&transaction.Output{ + AssetID: GoverningTokenID(), + Amount: neoAmount, + ScriptHash: priv0.GetScriptHash(), + Position: 0, + }) + txMoveNeo.AddOutput(&transaction.Output{ + AssetID: GoverningTokenID(), + Amount: neoRemainder, + ScriptHash: scriptHash, + Position: 1, + }) + txMoveNeo.Data = new(transaction.ContractTX) + + validators, err := getValidators(bc.config) + require.NoError(t, err) + rawScript, err := smartcontract.CreateMultiSigRedeemScript(len(bc.config.StandbyValidators)/2+1, validators) + require.NoError(t, err) + data := txMoveNeo.GetSignedPart() + + var invoc []byte + for i := range privNetKeys { + priv, err := keys.NewPrivateKeyFromWIF(privNetKeys[i]) + require.NoError(t, err) + invoc = append(invoc, getInvocationScript(data, priv)...) + } + + txMoveNeo.Scripts = []transaction.Witness{{ + InvocationScript: invoc, + VerificationScript: rawScript, + }} + + b := bc.newBlock(newMinerTX(), txMoveNeo) + require.NoError(t, bc.AddBlock(b)) + t.Logf("txMoveNeo: %s", txMoveNeo.Hash().StringLE()) + + // Generate some blocks to be able to claim GAS for them. + _, err = bc.genBlocks(numOfEmptyBlocks) require.NoError(t, err) - tx1 := newMinerTX() + acc0, err := wallet.NewAccountFromWIF(priv0.WIF()) + require.NoError(t, err) + // Make a NEO roundtrip (send to myself) and claim GAS. + txNeoRound := transaction.NewContractTX() + txNeoRound.AddInput(&transaction.Input{ + PrevHash: txMoveNeo.Hash(), + PrevIndex: 0, + }) + txNeoRound.AddOutput(&transaction.Output{ + AssetID: GoverningTokenID(), + Amount: neoAmount, + ScriptHash: priv0.GetScriptHash(), + Position: 0, + }) + txNeoRound.Data = new(transaction.ContractTX) + require.NoError(t, acc0.SignTx(txNeoRound)) + b = bc.newBlock(newMinerTX(), txNeoRound) + require.NoError(t, bc.AddBlock(b)) + t.Logf("txNeoRound: %s", txNeoRound.Hash().StringLE()) + + txClaim := &transaction.Transaction{Type: transaction.ClaimType} + claim := new(transaction.ClaimTX) + claim.Claims = append(claim.Claims, transaction.Input{ + PrevHash: txMoveNeo.Hash(), + PrevIndex: 0, + }) + txClaim.Data = claim + neoGas, sysGas, err := bc.CalculateClaimable(neoAmount, 1, bc.BlockHeight()) + require.NoError(t, err) + gasOwned := neoGas + sysGas + txClaim.AddOutput(&transaction.Output{ + AssetID: UtilityTokenID(), + Amount: gasOwned, + ScriptHash: priv0.GetScriptHash(), + Position: 0, + }) + require.NoError(t, acc0.SignTx(txClaim)) + b = bc.newBlock(newMinerTX(), txClaim) + require.NoError(t, bc.AddBlock(b)) + t.Logf("txClaim: %s", txClaim.Hash().StringLE()) + + // Push some contract into the chain. avm, err := ioutil.ReadFile(prefix + "test_contract.avm") require.NoError(t, err) @@ -200,11 +297,25 @@ func _(t *testing.T) { emit.Syscall(script.BinWriter, "Neo.Contract.Create") txScript := script.Bytes() - tx2 := transaction.NewInvocationTX(txScript, util.Fixed8FromFloat(100)) - - block := bc.newBlock(tx1, tx2) - require.NoError(t, bc.AddBlock(block)) + invFee := util.Fixed8FromFloat(100) + txDeploy := transaction.NewInvocationTX(txScript, invFee) + txDeploy.AddInput(&transaction.Input{ + PrevHash: txClaim.Hash(), + PrevIndex: 0, + }) + txDeploy.AddOutput(&transaction.Output{ + AssetID: UtilityTokenID(), + Amount: gasOwned - invFee, + ScriptHash: priv0.GetScriptHash(), + Position: 0, + }) + gasOwned -= invFee + require.NoError(t, acc0.SignTx(txDeploy)) + b = bc.newBlock(newMinerTX(), txDeploy) + require.NoError(t, bc.AddBlock(b)) + t.Logf("txDeploy: %s", txDeploy.Hash().StringLE()) + // Now invoke this contract. script = io.NewBufBinWriter() emit.String(script.BinWriter, "testvalue") emit.String(script.BinWriter, "testkey") @@ -213,94 +324,54 @@ func _(t *testing.T) { emit.String(script.BinWriter, "Put") emit.AppCall(script.BinWriter, hash.Hash160(avm), false) - tx3 := transaction.NewInvocationTX(script.Bytes(), util.Fixed8FromFloat(100)) - - tx4 := transaction.NewContractTX() - h, err := util.Uint256DecodeStringBE("6da730b566db183bfceb863b780cd92dee2b497e5a023c322c1eaca81cf9ad7a") - require.NoError(t, err) - tx4.AddInput(&transaction.Input{ - PrevHash: h, - PrevIndex: 0, - }) - - // multisig address which possess all NEO - scriptHash, err := util.Uint160DecodeStringBE("be48d3a3f5d10013ab9ffee489706078714f1ea2") - require.NoError(t, err) - priv, err := keys.NewPrivateKeyFromWIF(privNetKeys[0]) - require.NoError(t, err) - tx4.AddOutput(&transaction.Output{ - AssetID: GoverningTokenID(), - Amount: util.Fixed8FromInt64(1000), - ScriptHash: priv.GetScriptHash(), - Position: 0, - }) - tx4.AddOutput(&transaction.Output{ - AssetID: GoverningTokenID(), - Amount: util.Fixed8FromInt64(99999000), - ScriptHash: scriptHash, - Position: 1, - }) - tx4.Data = new(transaction.ContractTX) - - validators, err := getValidators(bc.config) - require.NoError(t, err) - rawScript, err := smartcontract.CreateMultiSigRedeemScript(len(bc.config.StandbyValidators)/2+1, validators) - require.NoError(t, err) - data := tx4.GetSignedPart() - - var invoc []byte - for i := range privNetKeys { - priv, err := keys.NewPrivateKeyFromWIF(privNetKeys[i]) - require.NoError(t, err) - invoc = append(invoc, getInvocationScript(data, priv)...) - } - - tx4.Scripts = []transaction.Witness{{ - InvocationScript: invoc, - VerificationScript: rawScript, - }} - - b := bc.newBlock(newMinerTX(), tx3, tx4) + txInv := transaction.NewInvocationTX(script.Bytes(), 0) + b = bc.newBlock(newMinerTX(), txInv) require.NoError(t, bc.AddBlock(b)) + t.Logf("txInv: %s", txInv.Hash().StringLE()) priv1, err := keys.NewPrivateKeyFromWIF(privNetKeys[1]) require.NoError(t, err) - tx5 := transaction.NewContractTX() - tx5.Data = new(transaction.ContractTX) - tx5.AddInput(&transaction.Input{ - PrevHash: tx4.Hash(), + txNeo0to1 := transaction.NewContractTX() + txNeo0to1.Data = new(transaction.ContractTX) + txNeo0to1.AddInput(&transaction.Input{ + PrevHash: txNeoRound.Hash(), PrevIndex: 0, }) - tx5.AddOutput(&transaction.Output{ + txNeo0to1.AddOutput(&transaction.Output{ AssetID: GoverningTokenID(), Amount: util.Fixed8FromInt64(1000), ScriptHash: priv1.GetScriptHash(), }) + txNeo0to1.AddOutput(&transaction.Output{ + AssetID: GoverningTokenID(), + Amount: neoAmount - util.Fixed8FromInt64(1000), + ScriptHash: priv0.GetScriptHash(), + }) - acc, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) - require.NoError(t, acc.SignTx(tx5)) - b = bc.newBlock(newMinerTX(), tx5) + require.NoError(t, acc0.SignTx(txNeo0to1)) + b = bc.newBlock(newMinerTX(), txNeo0to1) require.NoError(t, bc.AddBlock(b)) - outStream, err := os.Create(prefix + "testblocks.acc") - require.NoError(t, err) - defer outStream.Close() - - writer := io.NewBinWriterFromIO(outStream) - - count := bc.BlockHeight() + 1 - writer.WriteU32LE(count - 1) - - for i := 1; i < int(count); i++ { - bh := bc.GetHeaderHash(i) - b, err := bc.GetBlock(bh) + if saveChain { + outStream, err := os.Create(prefix + "testblocks.acc") require.NoError(t, err) - buf := io.NewBufBinWriter() - b.EncodeBinary(buf.BinWriter) - bytes := buf.Bytes() - writer.WriteU32LE(uint32(len(bytes))) - writer.WriteBytes(bytes) - require.NoError(t, writer.Err) + defer outStream.Close() + + writer := io.NewBinWriterFromIO(outStream) + + count := bc.BlockHeight() + 1 + writer.WriteU32LE(count - 1) + + for i := 1; i < int(count); i++ { + bh := bc.GetHeaderHash(i) + b, err := bc.GetBlock(bh) + require.NoError(t, err) + buf := io.NewBufBinWriter() + b.EncodeBinary(buf.BinWriter) + bytes := buf.Bytes() + writer.WriteU32LE(uint32(len(bytes))) + writer.WriteBytes(bytes) + require.NoError(t, writer.Err) + } } } diff --git a/pkg/core/transaction/contract_test.go b/pkg/core/transaction/contract_test.go index fe3a7348e..dbdd21130 100644 --- a/pkg/core/transaction/contract_test.go +++ b/pkg/core/transaction/contract_test.go @@ -23,7 +23,8 @@ func TestEncodeDecodeContract(t *testing.T) { assert.Equal(t, "eec17cc828d6ede932b57e4eaf79c2591151096a7825435cd67f498f9fa98d88", input.PrevHash.StringLE()) assert.Equal(t, 0, int(input.PrevIndex)) - assert.Equal(t, int64(706), tx.Outputs[0].Amount.Int64Value()) + assert.Equal(t, int64(706), tx.Outputs[0].Amount.IntegralValue()) + assert.Equal(t, int32(0), tx.Outputs[0].Amount.FractionalValue()) assert.Equal(t, "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", tx.Outputs[0].AssetID.StringLE()) assert.Equal(t, "a8666b4830229d6a1a9b80f6088059191c122d2b", tx.Outputs[0].ScriptHash.String()) assert.Equal(t, "bdf6cc3b9af12a7565bda80933a75ee8cef1bc771d0d58effc08e4c8b436da79", tx.Hash().StringLE()) diff --git a/pkg/core/transaction/invocation.go b/pkg/core/transaction/invocation.go index 1306dbbe8..a73b74bad 100644 --- a/pkg/core/transaction/invocation.go +++ b/pkg/core/transaction/invocation.go @@ -1,6 +1,8 @@ package transaction import ( + "errors" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -36,8 +38,16 @@ func NewInvocationTX(script []byte, gas util.Fixed8) *Transaction { // DecodeBinary implements Serializable interface. func (tx *InvocationTX) DecodeBinary(br *io.BinReader) { tx.Script = br.ReadVarBytes() + if br.Err == nil && len(tx.Script) == 0 { + br.Err = errors.New("no script") + return + } if tx.Version >= 1 { tx.Gas.DecodeBinary(br) + if br.Err == nil && tx.Gas.LessThan(0) { + br.Err = errors.New("negative gas") + return + } } else { tx.Gas = util.Fixed8FromInt64(0) } diff --git a/pkg/core/transaction/invocation_test.go b/pkg/core/transaction/invocation_test.go new file mode 100644 index 000000000..a5e46f464 --- /dev/null +++ b/pkg/core/transaction/invocation_test.go @@ -0,0 +1,70 @@ +package transaction + +import ( + "encoding/hex" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInvocationZeroScript(t *testing.T) { + // Zero-length script. + in, err := hex.DecodeString("000000000000000000") + require.NoError(t, err) + + inv := &InvocationTX{Version: 1} + r := io.NewBinReaderFromBuf(in) + + inv.DecodeBinary(r) + assert.Error(t, r.Err) + + // PUSH1 script. + in, err = hex.DecodeString("01510000000000000000") + require.NoError(t, err) + r = io.NewBinReaderFromBuf(in) + + inv.DecodeBinary(r) + assert.NoError(t, r.Err) +} + +func TestInvocationNegativeGas(t *testing.T) { + // Negative GAS + in, err := hex.DecodeString("015100000000000000ff") + require.NoError(t, err) + + inv := &InvocationTX{Version: 1} + r := io.NewBinReaderFromBuf(in) + + inv.DecodeBinary(r) + assert.Error(t, r.Err) + + // Positive GAS. + in, err = hex.DecodeString("01510100000000000000") + require.NoError(t, err) + r = io.NewBinReaderFromBuf(in) + + inv.DecodeBinary(r) + assert.NoError(t, r.Err) + assert.Equal(t, util.Fixed8(1), inv.Gas) +} + +func TestInvocationVersionZero(t *testing.T) { + in, err := hex.DecodeString("0151") + require.NoError(t, err) + + inv := &InvocationTX{Version: 1} + r := io.NewBinReaderFromBuf(in) + + inv.DecodeBinary(r) + assert.Error(t, r.Err) + + inv = &InvocationTX{Version: 0} + r = io.NewBinReaderFromBuf(in) + + inv.DecodeBinary(r) + assert.NoError(t, r.Err) + assert.Equal(t, util.Fixed8(0), inv.Gas) +} diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 05a93da14..fb39e0fcf 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -44,14 +44,15 @@ var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { { name: "positive", - params: `["d5cf936296de912aa4d051531bd8d25c7a58fb68fc7f87c8d3e6e85475187c08"]`, + params: `["2441c2776cbab65bf81d38a839cf3a85689421631d4ba091be64703f02867315"]`, result: func(e *executor) interface{} { return &result.ApplicationLog{} }, check: func(t *testing.T, e *executor, acc interface{}) { res, ok := acc.(*result.ApplicationLog) require.True(t, ok) - expectedTxHash := util.Uint256{0x8, 0x7c, 0x18, 0x75, 0x54, 0xe8, 0xe6, 0xd3, 0xc8, 0x87, 0x7f, 0xfc, 0x68, 0xfb, 0x58, 0x7a, 0x5c, 0xd2, 0xd8, 0x1b, 0x53, 0x51, 0xd0, 0xa4, 0x2a, 0x91, 0xde, 0x96, 0x62, 0x93, 0xcf, 0xd5} + expectedTxHash, err := util.Uint256DecodeStringLE("2441c2776cbab65bf81d38a839cf3a85689421631d4ba091be64703f02867315") + require.NoError(t, err) assert.Equal(t, expectedTxHash, res.TxHash) assert.Equal(t, 1, len(res.Executions)) assert.Equal(t, "Application", res.Executions[0].Trigger) @@ -256,13 +257,13 @@ var rpcTestCases = map[string][]rpcTestCase{ "getblock": { { name: "positive", - params: "[1, 1]", + params: "[2, 1]", result: func(e *executor) interface{} { return &result.Block{} }, check: func(t *testing.T, e *executor, blockRes interface{}) { res, ok := blockRes.(*result.Block) require.True(t, ok) - block, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) + block, err := e.chain.GetBlock(e.chain.GetHeaderHash(2)) require.NoErrorf(t, err, "could not get block") assert.Equal(t, block.Hash(), res.Hash) @@ -381,13 +382,13 @@ var rpcTestCases = map[string][]rpcTestCase{ result: func(*executor) interface{} { // hash of the issueTx h, _ := util.Uint256DecodeStringBE("6da730b566db183bfceb863b780cd92dee2b497e5a023c322c1eaca81cf9ad7a") - amount := util.Fixed8FromInt64(52 * 8) // (endHeight - startHeight) * genAmount[0] + amount := util.Fixed8FromInt64(1 * 8) // (endHeight - startHeight) * genAmount[0] return &result.ClaimableInfo{ Spents: []result.Claimable{ { Tx: h, Value: util.Fixed8FromInt64(100000000), - EndHeight: 52, + EndHeight: 1, Generated: amount, Unclaimed: amount, }, diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 9f136a2b4..5507af4ea 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/util/fixed8.go b/pkg/util/fixed8.go index 6604f0f38..c511f76de 100644 --- a/pkg/util/fixed8.go +++ b/pkg/util/fixed8.go @@ -46,11 +46,18 @@ func (f Fixed8) FloatValue() float64 { return float64(f) / decimals } -// Int64Value returns the original value representing Fixed8 as int64. -func (f Fixed8) Int64Value() int64 { +// IntegralValue returns integer part of the original value representing +// Fixed8 as int64. +func (f Fixed8) IntegralValue() int64 { return int64(f) / decimals } +// FractionalValue returns decimal part of the original value. It has the same +// sign as f, so that f = f.IntegralValue() + f.FractionalValue(). +func (f Fixed8) FractionalValue() int32 { + return int32(int64(f) % decimals) +} + // Fixed8FromInt64 returns a new Fixed8 type multiplied by decimals. func Fixed8FromInt64(val int64) Fixed8 { return Fixed8(decimals * val) diff --git a/pkg/util/fixed8_test.go b/pkg/util/fixed8_test.go index 893cc86dc..5f01dee12 100644 --- a/pkg/util/fixed8_test.go +++ b/pkg/util/fixed8_test.go @@ -2,6 +2,7 @@ package util import ( "encoding/json" + "math" "strconv" "testing" @@ -16,7 +17,8 @@ func TestFixed8FromInt64(t *testing.T) { for _, val := range values { assert.Equal(t, Fixed8(val*decimals), Fixed8FromInt64(val)) - assert.Equal(t, val, Fixed8FromInt64(val).Int64Value()) + assert.Equal(t, val, Fixed8FromInt64(val).IntegralValue()) + assert.Equal(t, int32(0), Fixed8FromInt64(val).FractionalValue()) } } @@ -35,7 +37,8 @@ func TestFixed8Sub(t *testing.T) { b := Fixed8FromInt64(34) c := a.Sub(b) - assert.Equal(t, int64(8), c.Int64Value()) + assert.Equal(t, int64(8), c.IntegralValue()) + assert.Equal(t, int32(0), c.FractionalValue()) } func TestFixed8FromFloat(t *testing.T) { @@ -44,6 +47,10 @@ func TestFixed8FromFloat(t *testing.T) { for _, val := range inputs { assert.Equal(t, Fixed8(val*decimals), Fixed8FromFloat(val)) assert.Equal(t, val, Fixed8FromFloat(val).FloatValue()) + trunc := math.Trunc(val) + rem := (val - trunc) * decimals + assert.Equal(t, int64(trunc), Fixed8FromFloat(val).IntegralValue()) + assert.Equal(t, int32(math.Round(rem)), Fixed8FromFloat(val).FractionalValue()) } }