diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 1433c1fe6..e677c6959 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -2,14 +2,20 @@ package wallet import ( "bufio" + "context" "errors" "fmt" "os" "strings" "syscall" + "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/encoding/address" + "github.com/CityOfZion/neo-go/pkg/rpc/client" + "github.com/CityOfZion/neo-go/pkg/rpc/request" + "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -33,6 +39,14 @@ var ( Name: "decrypt, d", Usage: "Decrypt encrypted keys.", } + rpcFlag = cli.StringFlag{ + Name: "rpc, r", + Usage: "RPC node address", + } + timeoutFlag = cli.DurationFlag{ + Name: "timeout, t", + Usage: "Timeout for the operation", + } ) // NewCommands returns 'wallet' command. @@ -41,6 +55,20 @@ func NewCommands() []cli.Command { Name: "wallet", Usage: "create, open and manage a NEO wallet", Subcommands: []cli.Command{ + { + Name: "claim", + Usage: "claim GAS", + Action: claimGas, + Flags: []cli.Flag{ + walletPathFlag, + rpcFlag, + timeoutFlag, + cli.StringFlag{ + Name: "address, a", + Usage: "Address to claim GAS for", + }, + }, + }, { Name: "create", Usage: "create a new wallet", @@ -112,10 +140,106 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "transfer", + Usage: "transfer NEO/GAS", + UsageText: "transfer --path --from --to " + + " --amount --asset [NEO|GAS|]", + Action: transferAsset, + Flags: []cli.Flag{ + walletPathFlag, + rpcFlag, + timeoutFlag, + cli.StringFlag{ + Name: "from", + Usage: "Address to send an asset from", + }, + cli.StringFlag{ + Name: "to", + Usage: "Address to send an asset to", + }, + cli.StringFlag{ + Name: "amount", + Usage: "Amount of asset to send", + }, + cli.StringFlag{ + Name: "asset", + Usage: "Asset ID", + }, + }, + }, }, }} } +func claimGas(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + addr := ctx.String("address") + scriptHash, err := address.StringToUint160(addr) + if err != nil { + return cli.NewExitError(err, 1) + } + + acc := wall.GetAccount(scriptHash) + if acc == nil { + return cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", addr), 1) + } + + pass, err := readPassword("Enter password > ") + if err != nil { + return cli.NewExitError(err, 1) + } else if err := acc.Decrypt(pass); err != nil { + return cli.NewExitError(err, 1) + } + + gctx, cancel := getGoContext(ctx) + defer cancel() + + c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + if err != nil { + return cli.NewExitError(err, 1) + } + info, err := c.GetClaimable(addr) + if err != nil { + return cli.NewExitError(err, 1) + } else if info.Unclaimed == 0 || len(info.Spents) == 0 { + fmt.Println("Nothing to claim") + return nil + } + + var claim transaction.ClaimTX + for i := range info.Spents { + claim.Claims = append(claim.Claims, transaction.Input{ + PrevHash: info.Spents[i].Tx, + PrevIndex: uint16(info.Spents[i].N), + }) + } + + tx := &transaction.Transaction{ + Type: transaction.ClaimType, + Data: &claim, + } + + tx.AddOutput(&transaction.Output{ + AssetID: core.UtilityTokenID(), + Amount: info.Unclaimed, + ScriptHash: scriptHash, + }) + + _ = acc.SignTx(tx) + if err := c.SendRawTransaction(tx); err != nil { + return cli.NewExitError(err, 1) + } + + fmt.Println(tx.Hash().StringLE()) + return nil +} + func addAccount(ctx *cli.Context) error { wall, err := openWallet(ctx.String("path")) if err != nil { @@ -249,6 +373,81 @@ func importWallet(ctx *cli.Context) error { return nil } +func transferAsset(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + from := ctx.String("from") + addr, err := address.StringToUint160(from) + if err != nil { + return cli.NewExitError("invalid address", 1) + } + acc := wall.GetAccount(addr) + if acc == nil { + return cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", addr), 1) + } + + asset, err := getAssetID(ctx.String("asset")) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid asset id: %v", err), 1) + } + + amount, err := util.Fixed8FromString(ctx.String("amount")) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid amount: %v", err), 1) + } + + pass, err := readPassword("Enter wallet password > ") + if err != nil { + return cli.NewExitError(err, 1) + } else if err := acc.Decrypt(pass); err != nil { + return cli.NewExitError(err, 1) + } + + gctx, cancel := getGoContext(ctx) + defer cancel() + + c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + if err != nil { + return cli.NewExitError(err, 1) + } + + tx := transaction.NewContractTX() + tx.Data = new(transaction.ContractTX) + if err := request.AddInputsAndUnspentsToTx(tx, from, asset, amount, c); err != nil { + return cli.NewExitError(err, 1) + } + + toAddr, err := address.StringToUint160(ctx.String("to")) + if err != nil { + return cli.NewExitError(err, 1) + } + tx.AddOutput(&transaction.Output{ + AssetID: asset, + Amount: amount, + ScriptHash: toAddr, + Position: 1, + }) + + _ = acc.SignTx(tx) + if err := c.SendRawTransaction(tx); err != nil { + return cli.NewExitError(err, 1) + } + + fmt.Println(tx.Hash().StringLE()) + return nil +} + +func getGoContext(ctx *cli.Context) (context.Context, func()) { + if dur := ctx.Duration("timeout"); dur != 0 { + return context.WithTimeout(context.Background(), dur) + } + return context.Background(), func() {} +} + func dumpWallet(ctx *cli.Context) error { wall, err := openWallet(ctx.String("path")) if err != nil { @@ -331,6 +530,18 @@ func openWallet(path string) (*wallet.Wallet, error) { return wallet.NewWalletFromFile(path) } +func getAssetID(s string) (util.Uint256, error) { + s = strings.ToLower(s) + switch { + case s == "neo": + return core.GoverningTokenID(), nil + case s == "gas": + return core.UtilityTokenID(), nil + default: + return util.Uint256DecodeStringLE(s) + } +} + func newAccountFromWIF(wif string) (*wallet.Account, error) { // note: NEP2 strings always have length of 58 even though // base58 strings can have different lengths even if slice lengths are equal diff --git a/integration/performance_test.go b/integration/performance_test.go index e75e45087..7945ddfce 100644 --- a/integration/performance_test.go +++ b/integration/performance_test.go @@ -11,7 +11,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/CityOfZion/neo-go/pkg/network" - "github.com/CityOfZion/neo-go/pkg/rpc/request" + "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -49,10 +49,12 @@ func prepareData(t *testing.B) []*transaction.Transaction { var data []*transaction.Transaction wif := getWif(t) + acc, err := wallet.NewAccountFromWIF(wif.S) + require.NoError(t, err) for n := 0; n < t.N; n++ { tx := getTX(t, wif) - require.NoError(t, request.SignTx(tx, wif)) + require.NoError(t, acc.SignTx(tx)) data = append(data, tx) } return data diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index fdb57dfc6..d2256f70b 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -476,16 +476,23 @@ func (s *service) getVerifiedTx(count int) []block.Transaction { } func (s *service) getValidators(txx ...block.Transaction) []crypto.PublicKey { - var pKeys []*keys.PublicKey + var ( + pKeys []*keys.PublicKey + err error + ) if len(txx) == 0 { - pKeys, _ = s.Chain.GetValidators() + pKeys, err = s.Chain.GetValidators() } else { ntxx := make([]*transaction.Transaction, len(txx)) for i := range ntxx { ntxx[i] = txx[i].(*transaction.Transaction) } - pKeys, _ = s.Chain.GetValidators(ntxx...) + pKeys, err = s.Chain.GetValidators(ntxx...) + } + + if err != nil { + s.log.Error("error while trying to get validators", zap.Error(err)) } pubs := make([]crypto.PublicKey, len(pKeys)) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 29ee9a4c6..b8a53c393 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -29,7 +29,7 @@ import ( // Tuning parameters. const ( headerBatchCount = 2000 - version = "0.0.3" + version = "0.0.5" // This one comes from C# code and it's different from the constant used // when creating an asset with Neo.Asset.Create interop call. It looks @@ -93,6 +93,9 @@ type Blockchain struct { // Number of headers stored in the chain file. storedHeaderCount uint32 + generationAmount []int + decrementInterval int + // All operations on headerList must be called from an // headersOp to be routine safe. headerList *HeaderHashList @@ -154,6 +157,9 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L memPool: mempool.NewMemPool(cfg.MemPoolSize), keyCache: make(map[util.Uint160]map[string]*keys.PublicKey), log: log, + + generationAmount: genAmount, + decrementInterval: decrementInterval, } if err := bc.init(); err != nil { @@ -408,6 +414,7 @@ func (bc *Blockchain) processHeader(h *block.Header, batch storage.Batch, header } buf.Reset() + buf.BinWriter.WriteU32LE(0) // sys fee is yet to be calculated h.EncodeBinary(buf.BinWriter) if buf.Err != nil { return buf.Err @@ -420,13 +427,24 @@ func (bc *Blockchain) processHeader(h *block.Header, batch storage.Batch, header return nil } +// bc.GetHeaderHash(int(endHeight)) returns sum of all system fees for blocks up to h. +// and 0 if no such block exists. +func (bc *Blockchain) getSystemFeeAmount(h util.Uint256) uint32 { + _, sf, _ := bc.dao.GetBlock(h) + return sf +} + // TODO: storeBlock needs some more love, its implemented as in the original // project. This for the sake of development speed and understanding of what // is happening here, quite allot as you can see :). If things are wired together // and all tests are in place, we can make a more optimized and cleaner implementation. func (bc *Blockchain) storeBlock(block *block.Block) error { cache := newCachedDao(bc.dao.store) - if err := cache.StoreAsBlock(block, 0); err != nil { + fee := bc.getSystemFeeAmount(block.PrevHash) + for _, tx := range block.Transactions { + fee += uint32(bc.SystemFee(tx).Int64Value()) + } + if err := cache.StoreAsBlock(block, fee); err != nil { return err } @@ -473,6 +491,13 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { } if prevTXOutput.AssetID.Equals(GoverningTokenID()) { + account.Unclaimed = append(account.Unclaimed, state.UnclaimedBalance{ + Tx: prevTX.Hash(), + Index: input.PrevIndex, + Start: prevTXHeight, + End: block.Index, + Value: prevTXOutput.Amount, + }) spentCoin.items[input.PrevIndex] = block.Index if err = processTXWithValidatorsSubtract(&prevTXOutput, account, cache); err != nil { return err @@ -564,6 +589,37 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { } break } + + prevTx, _, err := cache.GetTransaction(input.PrevHash) + if err != nil { + return err + } else if int(input.PrevIndex) > len(prevTx.Outputs) { + return errors.New("invalid input in claim") + } + acc, err := cache.GetAccountState(prevTx.Outputs[input.PrevIndex].ScriptHash) + if err != nil { + return err + } + + var changed bool + for i := range acc.Unclaimed { + if acc.Unclaimed[i].Tx == input.PrevHash && acc.Unclaimed[i].Index == input.PrevIndex { + copy(acc.Unclaimed[i:], acc.Unclaimed[i+1:]) + acc.Unclaimed = acc.Unclaimed[:len(acc.Unclaimed)-1] + changed = true + break + } + } + + if !changed { + bc.log.Warn("no spent coin in the account", + zap.String("tx", tx.Hash().StringLE()), + zap.String("input", input.PrevHash.StringLE()), + zap.String("account", acc.ScriptHash.String())) + } else if err := cache.PutAccountState(acc); err != nil { + return err + } + delete(scs.items, input.PrevIndex) if len(scs.items) > 0 { if err = cache.PutSpentCoinState(input.PrevHash, scs); err != nil { @@ -905,7 +961,7 @@ func (bc *Blockchain) GetBlock(hash util.Uint256) (*block.Block, error) { } } - block, err := bc.dao.GetBlock(hash) + block, _, err := bc.dao.GetBlock(hash) if err != nil { return nil, err } @@ -930,7 +986,7 @@ func (bc *Blockchain) GetHeader(hash util.Uint256) (*block.Header, error) { return tb.Header(), nil } } - block, err := bc.dao.GetBlock(hash) + block, _, err := bc.dao.GetBlock(hash) if err != nil { return nil, err } @@ -1033,6 +1089,48 @@ func (bc *Blockchain) GetConfig() config.ProtocolConfiguration { return bc.config } +// CalculateClaimable calculates the amount of GAS which can be claimed for a transaction with value. +// First return value is GAS generated between startHeight and endHeight. +// Second return value is GAS returned from accumulated SystemFees between startHeight and endHeight. +func (bc *Blockchain) CalculateClaimable(value util.Fixed8, startHeight, endHeight uint32) (util.Fixed8, util.Fixed8, error) { + var amount util.Fixed8 + di := uint32(bc.decrementInterval) + + ustart := startHeight / di + if genSize := uint32(len(bc.generationAmount)); ustart < genSize { + uend := endHeight / di + iend := endHeight % di + if uend >= genSize { + uend = genSize - 1 + iend = di + } else if iend == 0 { + uend-- + iend = di + } + + istart := startHeight % di + for ustart < uend { + amount += util.Fixed8(di-istart) * util.Fixed8(bc.generationAmount[ustart]) + ustart++ + istart = 0 + } + + amount += util.Fixed8(iend-istart) * util.Fixed8(bc.generationAmount[ustart]) + } + + if startHeight == 0 { + startHeight++ + } + h := bc.GetHeaderHash(int(startHeight - 1)) + feeStart := bc.getSystemFeeAmount(h) + h = bc.GetHeaderHash(int(endHeight - 1)) + feeEnd := bc.getSystemFeeAmount(h) + + sysFeeTotal := util.Fixed8(feeEnd - feeStart) + ratio := value / 100000000 + return amount * ratio, sysFeeTotal * ratio, nil +} + // References maps transaction's inputs into a slice of InOuts, effectively // joining each Input with the corresponding Output. // @TODO: unfortunately we couldn't attach this method to the Transaction struct in the @@ -1070,6 +1168,11 @@ func (bc *Blockchain) FeePerByte(t *transaction.Transaction) util.Fixed8 { // NetworkFee returns network fee. func (bc *Blockchain) NetworkFee(t *transaction.Transaction) util.Fixed8 { + // https://github.com/neo-project/neo/blob/master-2.x/neo/Network/P2P/Payloads/ClaimTransaction.cs#L16 + if t.Type == transaction.ClaimType || t.Type == transaction.MinerType { + return 0 + } + inputAmount := util.Fixed8FromInt64(0) refs, err := bc.References(t) if err != nil { @@ -1175,11 +1278,97 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e if bc.dao.IsDoubleClaim(claim) { return errors.New("double claim") } + if err := bc.verifyClaims(t); err != nil { + return err + } } return bc.verifyTxWitnesses(t, block) } +func (bc *Blockchain) verifyClaims(tx *transaction.Transaction) (err error) { + t := tx.Data.(*transaction.ClaimTX) + var result *transaction.Result + results := bc.GetTransactionResults(tx) + for i := range results { + if results[i].AssetID == UtilityTokenID() { + result = results[i] + break + } + } + + if result == nil || result.Amount.GreaterThan(0) { + return errors.New("invalid output in claim tx") + } + + bonus, err := bc.calculateBonus(t.Claims) + if err == nil && bonus != -result.Amount { + return fmt.Errorf("wrong bonus calculated in claim tx: %s != %s", + bonus.String(), (-result.Amount).String()) + } + + return err +} + +func (bc *Blockchain) calculateBonus(claims []transaction.Input) (util.Fixed8, error) { + unclaimed := []*spentCoin{} + inputs := transaction.GroupInputsByPrevHash(claims) + + for _, group := range inputs { + h := group[0].PrevHash + claimable, err := bc.getUnclaimed(h) + if err != nil || len(claimable) == 0 { + return 0, errors.New("no unclaimed inputs") + } + + for _, c := range group { + s, ok := claimable[c.PrevIndex] + if !ok { + return 0, fmt.Errorf("can't find spent coins for %s (%d)", c.PrevHash.StringLE(), c.PrevIndex) + } + unclaimed = append(unclaimed, s) + } + } + + return bc.calculateBonusInternal(unclaimed) +} + +func (bc *Blockchain) calculateBonusInternal(scs []*spentCoin) (util.Fixed8, error) { + var claimed util.Fixed8 + for _, sc := range scs { + gen, sys, err := bc.CalculateClaimable(sc.Output.Amount, sc.StartHeight, sc.EndHeight) + if err != nil { + return 0, err + } + claimed += gen + sys + } + + return claimed, nil +} + +func (bc *Blockchain) getUnclaimed(h util.Uint256) (map[uint16]*spentCoin, error) { + tx, txHeight, err := bc.GetTransaction(h) + if err != nil { + return nil, err + } + + scs, err := bc.dao.GetSpentCoinState(h) + if err != nil { + return nil, err + } + + result := make(map[uint16]*spentCoin) + for i, height := range scs.items { + result[i] = &spentCoin{ + Output: &tx.Outputs[i], + StartHeight: txHeight, + EndHeight: height, + } + } + + return result, nil +} + // isTxStillRelevant is a callback for mempool transaction filtering after the // new block addition. It returns false for transactions already present in the // chain (added by the new block), transactions using some inputs that are @@ -1396,7 +1585,7 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P for _, tx := range txes { // iterate through outputs for index, output := range tx.Outputs { - accountState, err := cache.GetAccountState(output.ScriptHash) + accountState, err := cache.GetAccountStateOrNew(output.ScriptHash) if err != nil { return nil, err } diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 523f3b448..614218737 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -175,6 +175,43 @@ func TestGetTransaction(t *testing.T) { } } +func TestGetClaimable(t *testing.T) { + bc := newTestChain(t) + + bc.generationAmount = []int{4, 3, 2, 1} + bc.decrementInterval = 2 + _, err := bc.genBlocks(10) + require.NoError(t, err) + + t.Run("first generation period", func(t *testing.T) { + amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 0, 2) + require.NoError(t, err) + require.EqualValues(t, 8, amount) + require.EqualValues(t, 0, sysfee) + }) + + t.Run("a number of full periods", func(t *testing.T) { + amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 0, 6) + require.NoError(t, err) + require.EqualValues(t, 4+4+3+3+2+2, amount) + require.EqualValues(t, 0, sysfee) + }) + + t.Run("start from the 2-nd block", func(t *testing.T) { + amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 1, 7) + require.NoError(t, err) + require.EqualValues(t, 4+3+3+2+2+1, amount) + require.EqualValues(t, 0, sysfee) + }) + + t.Run("end height after generation has ended", func(t *testing.T) { + amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 1, 10) + require.NoError(t, err) + require.EqualValues(t, 4+3+3+2+2+1+1, amount) + require.EqualValues(t, 0, sysfee) + }) +} + func TestClose(t *testing.T) { defer func() { r := recover() diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index bbe3b0c6b..d5368dd28 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -20,6 +20,7 @@ type Blockchainer interface { AddHeaders(...*block.Header) error AddBlock(*block.Block) error BlockHeight() uint32 + CalculateClaimable(value util.Fixed8, startHeight, endHeight uint32) (util.Fixed8, util.Fixed8, error) Close() HeaderHeight() uint32 GetBlock(hash util.Uint256) (*block.Block, error) diff --git a/pkg/core/dao.go b/pkg/core/dao.go index d84e50816..124316abe 100644 --- a/pkg/core/dao.go +++ b/pkg/core/dao.go @@ -375,17 +375,18 @@ func makeStorageItemKey(scripthash util.Uint160, key []byte) []byte { // -- other. // GetBlock returns Block by the given hash if it exists in the store. -func (dao *dao) GetBlock(hash util.Uint256) (*block.Block, error) { +func (dao *dao) GetBlock(hash util.Uint256) (*block.Block, uint32, error) { key := storage.AppendPrefix(storage.DataBlock, hash.BytesLE()) b, err := dao.store.Get(key) if err != nil { - return nil, err + return nil, 0, err } - block, err := block.NewBlockFromTrimmedBytes(b) + + block, err := block.NewBlockFromTrimmedBytes(b[4:]) if err != nil { - return nil, err + return nil, 0, err } - return block, err + return block, binary.LittleEndian.Uint32(b[:4]), nil } // GetVersion attempts to get the current version stored in the @@ -508,8 +509,7 @@ func (dao *dao) StoreAsBlock(block *block.Block, sysFee uint32) error { key = storage.AppendPrefix(storage.DataBlock, block.Hash().BytesLE()) buf = io.NewBufBinWriter() ) - // sysFee needs to be handled somehow - // buf.WriteU32LE(sysFee) + buf.WriteU32LE(sysFee) b, err := block.Trim() if err != nil { return err diff --git a/pkg/core/dao_test.go b/pkg/core/dao_test.go index 3b4a30343..da2964c45 100644 --- a/pkg/core/dao_test.go +++ b/pkg/core/dao_test.go @@ -254,7 +254,7 @@ func TestDeleteStorageItem(t *testing.T) { func TestGetBlock_NotExists(t *testing.T) { dao := newDao(storage.NewMemoryStore()) hash := random.Uint256() - block, err := dao.GetBlock(hash) + block, _, err := dao.GetBlock(hash) require.Error(t, err) require.Nil(t, block) } @@ -270,11 +270,12 @@ func TestPutGetBlock(t *testing.T) { }, } hash := b.Hash() - err := dao.StoreAsBlock(b, 0) + err := dao.StoreAsBlock(b, 42) require.NoError(t, err) - gotBlock, err := dao.GetBlock(hash) + gotBlock, sysfee, err := dao.GetBlock(hash) require.NoError(t, err) require.NotNil(t, gotBlock) + require.EqualValues(t, 42, sysfee) } func TestGetVersion_NoVersion(t *testing.T) { diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 78cd7bb34..4ec207744 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -20,6 +20,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm/emit" "github.com/CityOfZion/neo-go/pkg/vm/opcode" + "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) @@ -158,6 +159,11 @@ func newDumbBlock() *block.Block { } } +func getInvocationScript(data []byte, priv *keys.PrivateKey) []byte { + signature := priv.Sign(data) + return append([]byte{byte(opcode.PUSHBYTES64)}, signature...) +} + // This function generates "../rpc/testdata/testblocks.acc" file which contains data // for RPC unit tests. // To generate new "../rpc/testdata/testblocks.acc", follow the steps: @@ -165,6 +171,8 @@ func newDumbBlock() *block.Block { // 2. Add specific test-case into "neo-go/pkg/core/blockchain_test.go" // 3. Run tests with `$ make test` func _(t *testing.T) { + const prefix = "../rpc/server/testdata/" + bc := newTestChain(t) n := 50 _, err := bc.genBlocks(n) @@ -172,7 +180,7 @@ func _(t *testing.T) { tx1 := newMinerTX() - avm, err := ioutil.ReadFile("../rpc/testdata/test_contract.avm") + avm, err := ioutil.ReadFile(prefix + "test_contract.avm") require.NoError(t, err) var props smartcontract.PropertyState @@ -206,10 +214,76 @@ func _(t *testing.T) { emit.AppCall(script.BinWriter, hash.Hash160(avm), false) tx3 := transaction.NewInvocationTX(script.Bytes(), util.Fixed8FromFloat(100)) - b := bc.newBlock(newMinerTX(), tx3) + + 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) require.NoError(t, bc.AddBlock(b)) - outStream, err := os.Create("../rpc/testdata/testblocks.acc") + 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(), + PrevIndex: 0, + }) + tx5.AddOutput(&transaction.Output{ + AssetID: GoverningTokenID(), + Amount: util.Fixed8FromInt64(1000), + ScriptHash: priv1.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, bc.AddBlock(b)) + + outStream, err := os.Create(prefix + "testblocks.acc") require.NoError(t, err) defer outStream.Close() @@ -225,6 +299,7 @@ func _(t *testing.T) { 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/spent_coin_state.go b/pkg/core/spent_coin_state.go index def3cbbaa..53cbb8eee 100644 --- a/pkg/core/spent_coin_state.go +++ b/pkg/core/spent_coin_state.go @@ -1,6 +1,7 @@ package core import ( + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" ) @@ -14,6 +15,13 @@ type SpentCoinState struct { items map[uint16]uint32 } +// spentCoin represents the state of a single spent coin output. +type spentCoin struct { + Output *transaction.Output + StartHeight uint32 + EndHeight uint32 +} + // NewSpentCoinState returns a new SpentCoinState object. func NewSpentCoinState(hash util.Uint256, height uint32) *SpentCoinState { return &SpentCoinState{ diff --git a/pkg/core/state/account.go b/pkg/core/state/account.go index 12522b0d4..cbb07dac0 100644 --- a/pkg/core/state/account.go +++ b/pkg/core/state/account.go @@ -14,6 +14,16 @@ type UnspentBalance struct { Value util.Fixed8 `json:"value"` } +// UnclaimedBalance represents transaction output which was spent and +// can be claimed. +type UnclaimedBalance struct { + Tx util.Uint256 + Index uint16 + Start uint32 + End uint32 + Value util.Fixed8 +} + // UnspentBalances is a slice of UnspentBalance (mostly needed to sort them). type UnspentBalances []UnspentBalance @@ -24,6 +34,7 @@ type Account struct { IsFrozen bool Votes []*keys.PublicKey Balances map[util.Uint256][]UnspentBalance + Unclaimed []UnclaimedBalance } // NewAccount returns a new Account object. @@ -34,6 +45,7 @@ func NewAccount(scriptHash util.Uint160) *Account { IsFrozen: false, Votes: []*keys.PublicKey{}, Balances: make(map[util.Uint256][]UnspentBalance), + Unclaimed: []UnclaimedBalance{}, } } @@ -56,6 +68,8 @@ func (s *Account) DecodeBinary(br *io.BinReader) { } s.Balances[key] = ubs } + + br.ReadArray(&s.Unclaimed) } // EncodeBinary encodes Account to the given BinWriter. @@ -73,6 +87,8 @@ func (s *Account) EncodeBinary(bw *io.BinWriter) { v[i].EncodeBinary(bw) } } + + bw.WriteArray(s.Unclaimed) } // DecodeBinary implements io.Serializable interface. @@ -89,6 +105,24 @@ func (u *UnspentBalance) EncodeBinary(w *io.BinWriter) { u.Value.EncodeBinary(w) } +// DecodeBinary implements io.Serializable interface. +func (u *UnclaimedBalance) DecodeBinary(r *io.BinReader) { + u.Tx.DecodeBinary(r) + u.Index = r.ReadU16LE() + u.Start = r.ReadU32LE() + u.End = r.ReadU32LE() + u.Value.DecodeBinary(r) +} + +// EncodeBinary implements io.Serializable interface. +func (u *UnclaimedBalance) EncodeBinary(w *io.BinWriter) { + u.Tx.EncodeBinary(w) + w.WriteU16LE(u.Index) + w.WriteU32LE(u.Start) + w.WriteU32LE(u.End) + u.Value.EncodeBinary(w) +} + // GetBalanceValues sums all unspent outputs and returns a map of asset IDs to // overall balances. func (s *Account) GetBalanceValues() map[util.Uint256]util.Fixed8 { diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index cdabfa3c0..f20962bc3 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -100,6 +100,12 @@ func (t *Transaction) DecodeBinary(br *io.BinReader) { br.ReadArray(&t.Attributes) br.ReadArray(&t.Inputs) br.ReadArray(&t.Outputs) + for i := range t.Outputs { + if t.Outputs[i].Amount.LessThan(0) { + br.Err = errors.New("negative output") + return + } + } br.ReadArray(&t.Scripts) // Create the hash of the transaction at decode, so we dont need @@ -197,6 +203,16 @@ func (t Transaction) GroupOutputByAssetID() map[util.Uint256][]*Output { return m } +// GetSignedPart returns a part of the transaction which must be signed. +func (t *Transaction) GetSignedPart() []byte { + buf := io.NewBufBinWriter() + t.encodeHashableFields(buf.BinWriter) + if buf.Err != nil { + return nil + } + return buf.Bytes() +} + // Bytes converts the transaction to []byte func (t *Transaction) Bytes() []byte { buf := io.NewBufBinWriter() diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 1f72bee0d..3133a9f5c 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -32,6 +32,9 @@ func (chain testChain) ApplyPolicyToTxSet([]mempool.TxWithFee) []mempool.TxWithF func (chain testChain) GetConfig() config.ProtocolConfiguration { panic("TODO") } +func (chain testChain) CalculateClaimable(util.Fixed8, uint32, uint32) (util.Fixed8, util.Fixed8, error) { + panic("TODO") +} func (chain testChain) References(t *transaction.Transaction) ([]transaction.InOut, error) { panic("TODO") diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 85c04960c..5cb38dbeb 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -11,6 +11,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/rpc/response/result" "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/pkg/errors" ) @@ -43,6 +44,16 @@ func (c *Client) GetAccountState(address string) (*result.AccountState, error) { return resp, nil } +// GetClaimable returns tx outputs which can be claimed. +func (c *Client) GetClaimable(address string) (*result.ClaimableInfo, error) { + params := request.NewRawParams(address) + resp := new(result.ClaimableInfo) + if err := c.performRequest("getclaimable", params, resp); err != nil { + return nil, err + } + return resp, nil +} + // GetUnspents returns UTXOs for the given NEO account. func (c *Client) GetUnspents(address string) (*result.Unspents, error) { var ( @@ -108,11 +119,11 @@ func (c *Client) Invoke(script string, params []smartcontract.Parameter) (*respo // return resp, nil // } -// sendRawTransaction broadcasts a transaction over the NEO network. +// SendRawTransaction broadcasts a transaction over the NEO network. // The given hex string needs to be signed with a keypair. // When the result of the response object is true, the TX has successfully // been broadcasted to the network. -func (c *Client) sendRawTransaction(rawTX *transaction.Transaction) error { +func (c *Client) SendRawTransaction(rawTX *transaction.Transaction) error { var ( params = request.NewRawParams(hex.EncodeToString(rawTX.Bytes())) resp bool @@ -146,7 +157,7 @@ func (c *Client) TransferAsset(asset util.Uint256, address string, amount util.F if rawTx, err = request.CreateRawContractTransaction(txParams); err != nil { return resp, errors.Wrap(err, "failed to create raw transaction") } - if err = c.sendRawTransaction(rawTx); err != nil { + if err = c.SendRawTransaction(rawTx); err != nil { return resp, errors.Wrap(err, "failed to send raw transaction") } return rawTx.Hash(), nil @@ -169,11 +180,14 @@ func (c *Client) SignAndPushInvocationTx(script []byte, wif *keys.WIF, gas util. } } - if err = request.SignTx(tx, wif); err != nil { + acc, err := wallet.NewAccountFromWIF(wif.S) + if err != nil { + return txHash, err + } else if err = acc.SignTx(tx); err != nil { return txHash, errors.Wrap(err, "failed to sign tx") } txHash = tx.Hash() - err = c.sendRawTransaction(tx) + err = c.SendRawTransaction(tx) if err != nil { return txHash, errors.Wrap(err, "failed sendning tx") diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index dad08b42a..e7aa6c376 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -13,6 +13,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm/emit" "github.com/CityOfZion/neo-go/pkg/vm/opcode" + "github.com/CityOfZion/neo-go/pkg/wallet" errs "github.com/pkg/errors" ) @@ -48,7 +49,9 @@ func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transac } receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash) tx.AddOutput(receiverOutput) - if err = SignTx(tx, &wif); err != nil { + if acc, err := wallet.NewAccountFromWIF(wif.S); err != nil { + return nil, err + } else if err = acc.SignTx(tx); err != nil { return nil, errs.Wrap(err, "failed to sign tx") } @@ -77,36 +80,6 @@ func AddInputsAndUnspentsToTx(tx *transaction.Transaction, addr string, assetID return nil } -// SignTx signs given transaction in-place using given key. -func SignTx(tx *transaction.Transaction, wif *keys.WIF) error { - var witness transaction.Witness - var err error - - if witness.InvocationScript, err = GetInvocationScript(tx, wif); err != nil { - return errs.Wrap(err, "failed to create invocation script") - } - witness.VerificationScript = wif.PrivateKey.PublicKey().GetVerificationScript() - tx.Scripts = append(tx.Scripts, witness) - tx.Hash() - - return nil -} - -// GetInvocationScript returns NEO VM script containing transaction signature. -func GetInvocationScript(tx *transaction.Transaction, wif *keys.WIF) ([]byte, error) { - var ( - buf = io.NewBufBinWriter() - signature []byte - ) - tx.EncodeBinary(buf.BinWriter) - if buf.Err != nil { - return nil, errs.Wrap(buf.Err, "Failed to encode transaction to binary") - } - data := buf.Bytes() - signature = wif.PrivateKey.Sign(data[:(len(data) - 1)]) - return append([]byte{byte(opcode.PUSHBYTES64)}, signature...), nil -} - // CreateDeploymentScript returns a script that deploys given smart contract // with its metadata. func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) { diff --git a/pkg/rpc/response/result/claimable.go b/pkg/rpc/response/result/claimable.go new file mode 100644 index 000000000..d537a5891 --- /dev/null +++ b/pkg/rpc/response/result/claimable.go @@ -0,0 +1,22 @@ +package result + +import "github.com/CityOfZion/neo-go/pkg/util" + +// ClaimableInfo is a result of a getclaimable RPC call. +type ClaimableInfo struct { + Spents []Claimable `json:"claimable"` + Address string `json:"address"` + Unclaimed util.Fixed8 `json:"unclaimed"` +} + +// Claimable represents spent outputs which can be claimed. +type Claimable struct { + Tx util.Uint256 `json:"txid"` + N int `json:"n"` + Value util.Fixed8 `json:"value"` + StartHeight uint32 `json:"start_height"` + EndHeight uint32 `json:"end_height"` + Generated util.Fixed8 `json:"generated"` + SysFee util.Fixed8 `json:"sys_fee"` + Unclaimed util.Fixed8 `json:"unclaimed"` +} diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go index 2d095a322..00e7c313f 100644 --- a/pkg/rpc/server/prometheus.go +++ b/pkg/rpc/server/prometheus.go @@ -51,6 +51,14 @@ var ( }, ) + getclaimableCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getclaimable rpc endpoint", + Name: "getclaimable_called", + Namespace: "neogo", + }, + ) + getconnectioncountCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getconnectioncount rpc endpoint", diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index ebee6d090..2c055e5bf 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -190,6 +190,10 @@ Methods: getblocksysfeeCalled.Inc() results, resultsErr = s.getBlockSysFee(reqParams) + case "getclaimable": + getclaimableCalled.Inc() + results, resultsErr = s.getClaimable(reqParams) + case "getconnectioncount": getconnectioncountCalled.Inc() results = s.coreServer.PeerCount() @@ -322,6 +326,52 @@ func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, error return result.NewApplicationLog(appExecResult, scriptHash), nil } +func (s *Server) getClaimable(ps request.Params) (interface{}, error) { + p, ok := ps.ValueWithType(0, request.StringT) + if !ok { + return nil, response.ErrInvalidParams + } + u, err := p.GetUint160FromAddress() + if err != nil { + return nil, response.ErrInvalidParams + } + + var unclaimed []state.UnclaimedBalance + if acc := s.chain.GetAccountState(u); acc != nil { + unclaimed = acc.Unclaimed + } + + var sum util.Fixed8 + claimable := make([]result.Claimable, 0, len(unclaimed)) + for _, ub := range unclaimed { + gen, sys, err := s.chain.CalculateClaimable(ub.Value, ub.Start, ub.End) + if err != nil { + s.log.Info("error while calculating claim bonus", zap.Error(err)) + continue + } + + uc := gen.Add(sys) + sum += uc + + claimable = append(claimable, result.Claimable{ + Tx: ub.Tx, + N: int(ub.Index), + Value: ub.Value, + StartHeight: ub.Start, + EndHeight: ub.End, + Generated: gen, + SysFee: sys, + Unclaimed: uc, + }) + } + + return result.ClaimableInfo{ + Spents: claimable, + Address: p.String(), + Unclaimed: sum, + }, nil +} + func (s *Server) getStorage(ps request.Params) (interface{}, error) { param, ok := ps.Value(0) if !ok { diff --git a/pkg/rpc/server/server_helper_test.go b/pkg/rpc/server/server_helper_test.go index 1c24132da..cb4422a80 100644 --- a/pkg/rpc/server/server_helper_test.go +++ b/pkg/rpc/server/server_helper_test.go @@ -52,6 +52,7 @@ func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFu nBlocks = br.ReadU32LE() require.Nil(t, br.Err) for i := 0; i < int(nBlocks); i++ { + _ = br.ReadU32LE() b := &block.Block{} b.DecodeBinary(br) require.Nil(t, br.Err) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index e1b49b123..d7a8d9b3c 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -363,6 +363,40 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "getclaimable": { + { + name: "no params", + params: "[]", + fail: true, + }, + { + name: "invalid address", + params: `["invalid"]`, + fail: true, + }, + { + name: "normal address", + params: `["AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"]`, + result: func(*executor) interface{} { + // hash of the issueTx + h, _ := util.Uint256DecodeStringBE("6da730b566db183bfceb863b780cd92dee2b497e5a023c322c1eaca81cf9ad7a") + amount := util.Fixed8FromInt64(52 * 8) // (endHeight - startHeight) * genAmount[0] + return &result.ClaimableInfo{ + Spents: []result.Claimable{ + { + Tx: h, + Value: util.Fixed8FromInt64(100000000), + EndHeight: 52, + Generated: amount, + Unclaimed: amount, + }, + }, + Address: "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU", + Unclaimed: amount, + } + }, + }, + }, "getconnectioncount": { { params: "[]", diff --git a/pkg/rpc/testdata/test_contract.go b/pkg/rpc/server/testdata/test_contract.go similarity index 100% rename from pkg/rpc/testdata/test_contract.go rename to pkg/rpc/server/testdata/test_contract.go diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index f0bcc1f75..9f136a2b4 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/smartcontract/param_type.go b/pkg/smartcontract/param_type.go index e253ba846..0c98125fd 100644 --- a/pkg/smartcontract/param_type.go +++ b/pkg/smartcontract/param_type.go @@ -122,7 +122,7 @@ func (pt *ParamType) DecodeBinary(r *io.BinReader) { // bytes, bytearray -> ByteArrayType // key, publickey -> PublicKeyType // string -> StringType -// array -> ArrayType +// array, struct -> ArrayType // map -> MapType // interopinterface -> InteropInterfaceType // void -> VoidType @@ -145,7 +145,7 @@ func ParseParamType(typ string) (ParamType, error) { return PublicKeyType, nil case "string": return StringType, nil - case "array": + case "array", "struct": return ArrayType, nil case "map": return MapType, nil diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 6571dc837..7c19e0b33 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -214,7 +214,7 @@ func (i *BoolItem) MarshalJSON() ([]byte, error) { } func (i *BoolItem) String() string { - return "Bool" + return "Boolean" } // Dup implements StackItem interface. diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 90635e5c6..68a44363b 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -7,11 +7,13 @@ import ( "errors" "fmt" + "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/encoding/address" "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm/opcode" ) // Account represents a NEO account. It holds the private and public key @@ -122,6 +124,29 @@ func NewAccount() (*Account, error) { return newAccountFromPrivateKey(priv), nil } +// SignTx signs transaction t and updates it's Witnesses. +func (a *Account) SignTx(t *transaction.Transaction) error { + if a.privateKey == nil { + return errors.New("account is not unlocked") + } + data := t.GetSignedPart() + sign := a.privateKey.Sign(data) + + t.Scripts = append(t.Scripts, transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHBYTES64)}, sign...), + VerificationScript: a.getVerificationScript(), + }) + + return nil +} + +func (a *Account) getVerificationScript() []byte { + if a.Contract != nil { + return a.Contract.Script + } + return a.PrivateKey().PublicKey().GetVerificationScript() +} + // Decrypt decrypts the EncryptedWIF with the given passphrase returning error // if anything goes wrong. func (a *Account) Decrypt(passphrase string) error {