From 7095ec6c5147fac08bb036f65d386b61690f49a5 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 25 Feb 2020 16:15:17 +0300 Subject: [PATCH 01/22] core: implement (*Blockchain).CalculateClaimable Calculating amount of GAS that can be claimed is required for getclaimable RPC. --- pkg/core/blockchain.go | 55 +++++++++++++++++++++++++++++++++++++ pkg/core/blockchain_test.go | 40 +++++++++++++++++++++++++++ pkg/core/blockchainer.go | 1 + pkg/network/helper_test.go | 3 ++ 4 files changed, 99 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 29ee9a4c6..6239ad89f 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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 { @@ -1033,6 +1039,55 @@ 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]) + } + + var sysFeeTotal util.Fixed8 + if startHeight == 0 { + startHeight++ + } + for i := startHeight; i < endHeight; i++ { + h := bc.GetHeaderHash(int(i)) + b, err := bc.GetBlock(h) + if err != nil { + return 0, 0, err + } + for _, tx := range b.Transactions { + sysFeeTotal += bc.SystemFee(tx) + } + } + + sysFeeTotal /= 100000000 + 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 diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 523f3b448..0f1f38888 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -175,6 +175,46 @@ func TestGetTransaction(t *testing.T) { } } +func TestGetClaimable(t *testing.T) { + bc := newTestChain(t) + + _, _, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 0, 2) + require.Error(t, err) + + 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/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") From 95d9f36c98490ee1c749a76dd4cc49460c41517f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 25 Feb 2020 17:29:37 +0300 Subject: [PATCH 02/22] core: implement UnclaimedBalance tracking To make it easy to get unclaimed coins for the specified account they must be tracked together. --- pkg/core/blockchain.go | 40 ++++++++++++++++++++++++++++++++++++++- pkg/core/state/account.go | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 6239ad89f..4cae2ba98 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.4" // 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 @@ -479,6 +479,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 @@ -570,6 +577,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 { 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 { From 72d72296c385ecc79bb8f663ec951ae354fdc2da Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 10:05:13 +0300 Subject: [PATCH 03/22] core: implement (*Transaction).GetSignedPart() Marshalling it and taking all but last byte violates incapsulation and is just wrong in case transaction already contains any witnesses. --- pkg/core/transaction/transaction.go | 10 ++++++++++ pkg/rpc/request/txBuilder.go | 11 +---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index cdabfa3c0..e9384f50a 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -197,6 +197,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/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index dad08b42a..25738d7e5 100644 --- a/pkg/rpc/request/txBuilder.go +++ b/pkg/rpc/request/txBuilder.go @@ -94,16 +94,7 @@ func SignTx(tx *transaction.Transaction, wif *keys.WIF) error { // 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)]) + signature := wif.PrivateKey.Sign(tx.GetSignedPart()) return append([]byte{byte(opcode.PUSHBYTES64)}, signature...), nil } From 0e2a1f40badfc91399a7c14090f3f516b2c92970 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 26 Feb 2020 15:37:53 +0300 Subject: [PATCH 04/22] core: add ContractTX to a testchain generator Simple transfer from multisig account to the account of one of the validators. --- pkg/core/helper_test.go | 56 +++++++++++++++++- .../{ => server}/testdata/test_contract.go | 0 pkg/rpc/server/testdata/testblocks.acc | Bin 27236 -> 27799 bytes 3 files changed, 53 insertions(+), 3 deletions(-) rename pkg/rpc/{ => server}/testdata/test_contract.go (100%) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 78cd7bb34..35583ab28 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -165,6 +165,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 +174,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 +208,58 @@ 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) + signature := priv.Sign(data) + invoc = append(invoc, byte(opcode.PUSHBYTES64)) + invoc = append(invoc, signature...) + } + + 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") + outStream, err := os.Create(prefix + "testblocks.acc") require.NoError(t, err) defer outStream.Close() 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 f0bcc1f757a5ab0ed6b9bbbb4e873edf03c8df3e..2fe1d8be7c7dfa95e514301c3aa4ba3bf9631a3c 100644 GIT binary patch delta 16676 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 delta 16269 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 UWJOT8T7s&?|GT0lG1?0LAMXw4^#A|> From 51c486864157e321c6646b2caecded17dbb25644 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 26 Feb 2020 15:42:04 +0300 Subject: [PATCH 05/22] rpc: implement getclaimable RPC --- pkg/rpc/response/result/claimable.go | 22 ++++++++++++ pkg/rpc/server/prometheus.go | 8 +++++ pkg/rpc/server/server.go | 50 ++++++++++++++++++++++++++++ pkg/rpc/server/server_test.go | 34 +++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 pkg/rpc/response/result/claimable.go 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_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: "[]", From 44792ed5f748b8c57506ae8ca03dbe92c3cfbb7c Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 26 Feb 2020 17:51:30 +0300 Subject: [PATCH 06/22] core: make test chain format compatible with mainnet Write uint32 length before every block. --- pkg/core/helper_test.go | 1 + pkg/rpc/server/server_helper_test.go | 1 + pkg/rpc/server/testdata/testblocks.acc | Bin 27799 -> 28007 bytes 3 files changed, 2 insertions(+) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 35583ab28..7f3cb071b 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -275,6 +275,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/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/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 2fe1d8be7c7dfa95e514301c3aa4ba3bf9631a3c..06ac012bda2e9a2c57d5d7b43393594a37cdf5f0 100644 GIT binary patch delta 16631 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�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 delta 16414 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 Date: Thu, 27 Feb 2020 10:58:46 +0300 Subject: [PATCH 07/22] rpc: make (*Client).SendRawTransaction public --- pkg/rpc/client/rpc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 85c04960c..942fcafa7 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -108,11 +108,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 +146,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 @@ -173,7 +173,7 @@ func (c *Client) SignAndPushInvocationTx(script []byte, wif *keys.WIF, gas util. 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") From 45b8669b42af6c2cb23d45ada9d87b517d4f3cc0 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 27 Feb 2020 15:45:52 +0300 Subject: [PATCH 08/22] core: verify Claim transactions --- pkg/core/blockchain.go | 86 ++++++++++++++++++++++++++++++++++++ pkg/core/spent_coin_state.go | 8 ++++ 2 files changed, 94 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 4cae2ba98..4e45d2aa9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1268,11 +1268,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 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{ From 37c2bc473361535f0a0be18ba08b4bd156c686bf Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 27 Feb 2020 15:46:50 +0300 Subject: [PATCH 09/22] rpc: implement (*Client).GetClaimable() This is needed to form correct Claim transaction. --- pkg/rpc/client/rpc.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 942fcafa7..d12a23d03 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -43,6 +43,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 ( From 972e0d8ad1a286ebdb6d948c5a89332fda1b7a73 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 27 Feb 2020 15:59:17 +0300 Subject: [PATCH 10/22] core: add one more Contract tx to the test chain When testing CLI it is useful to have some spent coins on an account with a known key. --- pkg/core/helper_test.go | 31 ++++++++++++++++++++++--- pkg/rpc/server/testdata/testblocks.acc | Bin 28007 -> 28732 bytes 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 7f3cb071b..7814a616a 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -158,6 +158,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: @@ -246,9 +251,7 @@ func _(t *testing.T) { for i := range privNetKeys { priv, err := keys.NewPrivateKeyFromWIF(privNetKeys[i]) require.NoError(t, err) - signature := priv.Sign(data) - invoc = append(invoc, byte(opcode.PUSHBYTES64)) - invoc = append(invoc, signature...) + invoc = append(invoc, getInvocationScript(data, priv)...) } tx4.Scripts = []transaction.Witness{{ @@ -259,6 +262,28 @@ func _(t *testing.T) { b := bc.newBlock(newMinerTX(), tx3, tx4) require.NoError(t, bc.AddBlock(b)) + 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(), + }) + + tx5.Scripts = []transaction.Witness{{ + InvocationScript: getInvocationScript(tx5.GetSignedPart(), priv), + VerificationScript: priv.PublicKey().GetVerificationScript(), + }} + + 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() diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 06ac012bda2e9a2c57d5d7b43393594a37cdf5f0..9f136a2b4659b4c1ca73677212a542f294605568 100644 GIT binary patch delta 16716 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 delta 16227 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 k+bO(!T0c61 zn8?p=46}wwX$k%?O6E_nXdzauvy!1~(xHcVDM0x5PA@D5t>u*AqA2;mNOBLtB>oo4 zBU2tw75*Vgu_4F6lYz2jwcUWOOe5~??D&=aQisqdJLH`W>Wz*09~<#jp5Iiq&E*dc z8kxDFX?k{;zZ|~X|6T77-0N`&bLic8-XFp2H76ub#}IYbw2V^~eqr;vkkw?n6ZA!c z5^4viqDYQ7?jY&NYO2I9$hv*^x=cH%MCDSc$G0t7(R6uXZ;FJQV-7vIIv3Y964ELY8rtkuq&nh@Wimp>M(A7g|v@4Rc<`k=~NmNnZBg z>oln7kcMe9F=}^SU8hS#Lx?{&jm9JaN%;b6-dyE`({TZ>f5UF)pZ_q=e5Pltwct{E3`Tf!% zJHt-`9mm3i`E3$#G|v|T5K#1_QT$$?c{j;yQ3<49&rr|>zkxnVyOf@Jn4c9!xdTWX z6{GFJ1Nm_q|M7FoXD+865q`p^jZG&7>5sjXEWNui8hrZk`fVoo+Cr#!yF8G1FAPiB z*o@)=cIkP{3khrd-3|2S_VJ(FWOt=A9rD%d$sp@38!P<)zXcxbUBbNesK`$e(ox=- zB2n8vIMmgu?$&4nC;ydR->H(ZKyZ(&_kN)}(^JE#2XaGDwZ$~kAkW6Pe}ndXg#*O) zkJ-apx=$wA@7Q(gYVtjh|`nAp9i2pEv;MB#yp# z{V@quHb)f$J{{u-7GC-22s}sUPd8uqaVm?e%!Nw<`=zoQy+wN8GBom~$Bf}ojeCiR z)w$C|Yts;5`hr(!q<^b7v(`;7kX&x4ZH z)5s&DNL8S|K#L&5k(vw$p6=#?FQQS8&sA8T(_jZ`qFEZfez7`8v6knafDD!9#f}WSiNatx#CNQZYm=G0doCgDZ(*SgQk2jgKDc4dY%ZvT# zB_*PT*_ZAA!(k%Bc)QVP(`NA!`|doI@+17sdQs#r1nQGsSVIw$O0&Bc3ZxqaRvb}a z&v@HDeMS}k$FkWfyZUHp4U8};%THzq%&&p?!bIsfjGqYij$IElrFaX+$QJL3pARaH$&|r1VH1Kdnu1~42RCB({>&ar+jJk_?>b3Df#iwrZg6jI z|NV25G=O`-6)Z=#D#ZSPvK5>hX7pO7dLFN>Te4ji^4xfsIGWxcxG2vkYRcL}AA19e zkfhd6VZuRg+9Qr78EUu(JZ7-H+d^!-)AikJ4l}A!N~ic=j2_eds2N=&B5DkJz43)s|eOJ}4o)buF9uS>o2xpn045)p`Wou~<5ePY+~O z)R*Y>=9a3^0R{2?d^f^0$4$Zzn&4Oeb2NitWh4sHR%IU{lCnOZYch_J6c5hlJ{FDhuImRd=s^b-{NCLP6nV zWhV{3h)p#Ti&MKY{xMLhX+CMMjM^TU^Psp+v!WsgX})M>fKZ= zL(8!rP}%^g>eV@91D-BEA`+2J#IYs=A-vgf{IiSw6|?-+#HBw)U&R)&5CxHm%-)UR z{3CS2MQSu}tBO`-B(zjWD2>CdPIYp+uYdgrv&#)UzhO`u`?uAMr|uwtXho4~J*I1+ z943Ea;^?c!(dWXM4=3IMzXGAmq+rTxI}!#G8DVgWSH%0)%qj}|^YniivKg*Pc(Lz2 z{-FfVdIGci8ic-jSW%Lmo4-qBXDAY_m2G- Date: Thu, 27 Feb 2020 16:09:19 +0300 Subject: [PATCH 11/22] cli: add command for GAS claiming GAS can be claimed via `wallet claim` command. This will claim first get all claimable outputs via `getclaimable` RPC and then form a transaction signed byte the private key from the wallet. --- cli/wallet/wallet.go | 119 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 1433c1fe6..7e12c3a4e 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -2,14 +2,19 @@ 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/vm/opcode" "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -33,6 +38,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 +54,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", @@ -116,6 +143,74 @@ func NewCommands() []cli.Command { }} } +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, + }) + + signTx(tx, acc) + 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 +344,13 @@ func importWallet(ctx *cli.Context) error { 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 { @@ -295,6 +397,23 @@ func createWallet(ctx *cli.Context) error { return nil } +func signTx(tx *transaction.Transaction, acc *wallet.Account) { + priv := acc.PrivateKey() + sign := priv.Sign(tx.GetSignedPart()) + invoc := append([]byte{byte(opcode.PUSHBYTES64)}, sign...) + tx.Scripts = []transaction.Witness{{ + InvocationScript: invoc, + VerificationScript: getVerificationScript(acc), + }} +} + +func getVerificationScript(acc *wallet.Account) []byte { + if acc.Contract != nil { + return acc.Contract.Script + } + return acc.PrivateKey().PublicKey().GetVerificationScript() +} + func readAccountInfo() (string, string, error) { buf := bufio.NewReader(os.Stdin) fmt.Print("Enter the name of the account > ") From f69c8a763b4a78887355b679ba8a5c00a072ad6a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 27 Feb 2020 16:31:28 +0300 Subject: [PATCH 12/22] core: store system fee together with block Recalculating system fee can be rather costly if done frequently. --- pkg/core/blockchain.go | 7 ++++--- pkg/core/dao.go | 14 +++++++------- pkg/core/dao_test.go | 7 ++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 4e45d2aa9..5a4364b27 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -29,7 +29,7 @@ import ( // Tuning parameters. const ( headerBatchCount = 2000 - version = "0.0.4" + 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 @@ -414,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 @@ -949,7 +950,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 } @@ -974,7 +975,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 } 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) { From 258d8be1dda8dd3679da1e5ad226c5ddd7bf1c5a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 27 Feb 2020 16:42:17 +0300 Subject: [PATCH 13/22] core: store all accumulated SystemFee with every block --- pkg/core/blockchain.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 5a4364b27..b47c08e10 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -427,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 } From 1b9968df676e97be1da111446b05f28dd86aaad9 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 27 Feb 2020 16:48:24 +0300 Subject: [PATCH 14/22] core: optimize CalculateClaimable() Because accumulated system fee is stored for every block, it is easy to calculate sum with just to reads. --- pkg/core/blockchain.go | 17 +++++------------ pkg/core/blockchain_test.go | 5 +---- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index b47c08e10..8011a2af8 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1118,22 +1118,15 @@ func (bc *Blockchain) CalculateClaimable(value util.Fixed8, startHeight, endHeig amount += util.Fixed8(iend-istart) * util.Fixed8(bc.generationAmount[ustart]) } - var sysFeeTotal util.Fixed8 if startHeight == 0 { startHeight++ } - for i := startHeight; i < endHeight; i++ { - h := bc.GetHeaderHash(int(i)) - b, err := bc.GetBlock(h) - if err != nil { - return 0, 0, err - } - for _, tx := range b.Transactions { - sysFeeTotal += bc.SystemFee(tx) - } - } + h := bc.GetHeaderHash(int(startHeight - 1)) + feeStart := bc.getSystemFeeAmount(h) + h = bc.GetHeaderHash(int(endHeight - 1)) + feeEnd := bc.getSystemFeeAmount(h) - sysFeeTotal /= 100000000 + sysFeeTotal := util.Fixed8(feeEnd - feeStart) ratio := value / 100000000 return amount * ratio, sysFeeTotal * ratio, nil } diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 0f1f38888..614218737 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -178,12 +178,9 @@ func TestGetTransaction(t *testing.T) { func TestGetClaimable(t *testing.T) { bc := newTestChain(t) - _, _, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 0, 2) - require.Error(t, err) - bc.generationAmount = []int{4, 3, 2, 1} bc.decrementInterval = 2 - _, err = bc.genBlocks(10) + _, err := bc.genBlocks(10) require.NoError(t, err) t.Run("first generation period", func(t *testing.T) { From ed190dcfd302811b0a79ff68962f6dec021654b8 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 10:25:03 +0300 Subject: [PATCH 15/22] cli: add command for asset transfer It uses getunspents RPC for getting UTXO and forming transaction. --- cli/wallet/wallet.go | 110 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 7e12c3a4e..dc97a7661 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -14,6 +14,8 @@ import ( "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/vm/opcode" "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/urfave/cli" @@ -139,6 +141,34 @@ 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", + }, + }, + }, }, }} } @@ -344,6 +374,74 @@ 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, + }) + + signTx(tx, acc) + 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) @@ -450,6 +548,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 From de5215a564ed832dcd6b42826fe0fb5d342a2e46 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 11:08:46 +0300 Subject: [PATCH 16/22] core: use GetAccountStateOrNew() in (*Blockchain).GetValidators() Target of the transaction output may not yet exist in database. --- 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 8011a2af8..a7d8c0f5b 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1580,7 +1580,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 } From 21ef2638c0ca41b1b095c126c743ba17ae4a7a86 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 11:10:01 +0300 Subject: [PATCH 17/22] consensus: log error if GetValidators() failed --- pkg/consensus/consensus.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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)) From 9e9d59b49a5107626ee6e5a2c02be3364f3f293e Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 11:49:14 +0300 Subject: [PATCH 18/22] core: set NetworkFee to 0 for Claim and Miner transactions Claim tx have no GAS inputs and a positive output which can lead to negative network fee. --- pkg/core/blockchain.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index a7d8c0f5b..b8a53c393 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1168,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 { From 6add4f3e505d6d93688bace4c21ded198014dc75 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 16:53:26 +0300 Subject: [PATCH 19/22] transaction: disallow negative outputs Otherwise it is possible to make outputs which will sum to the expected value, but steal GAS from some other account. --- pkg/core/transaction/transaction.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index e9384f50a..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 From 6541bd4d4236a26dec59f0f4103420a630e426c9 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 18:21:20 +0300 Subject: [PATCH 20/22] vm: use `Boolean` in (*BoolItem).String() --- pkg/vm/stack_item.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 05a3625b7d69bbad681e61eabdb02a017b0f242b Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Feb 2020 19:01:07 +0300 Subject: [PATCH 21/22] wallet: implement (*Account).SignTx It is used in both CLI and RPC. --- cli/wallet/wallet.go | 22 ++-------------------- integration/performance_test.go | 6 ++++-- pkg/core/helper_test.go | 9 ++++----- pkg/rpc/client/rpc.go | 6 +++++- pkg/rpc/request/txBuilder.go | 26 ++++---------------------- pkg/wallet/account.go | 25 +++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 50 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index dc97a7661..e677c6959 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -16,7 +16,6 @@ import ( "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/vm/opcode" "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -232,7 +231,7 @@ func claimGas(ctx *cli.Context) error { ScriptHash: scriptHash, }) - signTx(tx, acc) + _ = acc.SignTx(tx) if err := c.SendRawTransaction(tx); err != nil { return cli.NewExitError(err, 1) } @@ -433,7 +432,7 @@ func transferAsset(ctx *cli.Context) error { Position: 1, }) - signTx(tx, acc) + _ = acc.SignTx(tx) if err := c.SendRawTransaction(tx); err != nil { return cli.NewExitError(err, 1) } @@ -495,23 +494,6 @@ func createWallet(ctx *cli.Context) error { return nil } -func signTx(tx *transaction.Transaction, acc *wallet.Account) { - priv := acc.PrivateKey() - sign := priv.Sign(tx.GetSignedPart()) - invoc := append([]byte{byte(opcode.PUSHBYTES64)}, sign...) - tx.Scripts = []transaction.Witness{{ - InvocationScript: invoc, - VerificationScript: getVerificationScript(acc), - }} -} - -func getVerificationScript(acc *wallet.Account) []byte { - if acc.Contract != nil { - return acc.Contract.Script - } - return acc.PrivateKey().PublicKey().GetVerificationScript() -} - func readAccountInfo() (string, string, error) { buf := bufio.NewReader(os.Stdin) fmt.Print("Enter the name of the account > ") 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/core/helper_test.go b/pkg/core/helper_test.go index 7814a616a..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" ) @@ -276,11 +277,9 @@ func _(t *testing.T) { ScriptHash: priv1.GetScriptHash(), }) - tx5.Scripts = []transaction.Witness{{ - InvocationScript: getInvocationScript(tx5.GetSignedPart(), priv), - VerificationScript: priv.PublicKey().GetVerificationScript(), - }} - + 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)) diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d12a23d03..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" ) @@ -179,7 +180,10 @@ 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() diff --git a/pkg/rpc/request/txBuilder.go b/pkg/rpc/request/txBuilder.go index 25738d7e5..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,27 +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) { - signature := wif.PrivateKey.Sign(tx.GetSignedPart()) - 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/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 { From 1264b629f1c3cbcf95542467369742e8538d1af6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 2 Mar 2020 18:05:26 +0300 Subject: [PATCH 22/22] smartcontract: parse Struct item type same way as Array --- pkg/smartcontract/param_type.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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