Merge pull request #718 from nspcc-dev/fix-invocation-tx-checks-and-fee

Fix Invocation TX checks and fee
This commit is contained in:
Roman Khimov 2020-03-05 11:51:51 +03:00 committed by GitHub
commit 5c781be08d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 280 additions and 101 deletions

View file

@ -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)

View file

@ -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)
}
}
}

View file

@ -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())

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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,
},

Binary file not shown.

View file

@ -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)

View file

@ -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())
}
}