From 71de8e970db2485ae93bcee01a0345068954aa45 Mon Sep 17 00:00:00 2001 From: Matt K <1036969+mkrump@users.noreply.github.com> Date: Mon, 4 Dec 2017 12:54:33 -0600 Subject: [PATCH] Contract hist (#84) Add ability to query contract historical state --- cmd/show_contract_summary/main.go | 17 ++++++++- integration_test/geth_blockchain_test.go | 4 +-- pkg/core/blockchain.go | 4 ++- pkg/fakes/blockchain.go | 29 ++++++++++----- pkg/geth/contract.go | 45 ++++++++++++++++-------- pkg/geth/contract_test.go | 32 +++++++++++++++-- pkg/watched_contracts/summary.go | 13 ++++--- pkg/watched_contracts/summary_test.go | 43 ++++++++++++++++------ 8 files changed, 143 insertions(+), 44 deletions(-) diff --git a/cmd/show_contract_summary/main.go b/cmd/show_contract_summary/main.go index 1bb5af9f..318fc124 100644 --- a/cmd/show_contract_summary/main.go +++ b/cmd/show_contract_summary/main.go @@ -7,6 +7,8 @@ import ( "fmt" + "math/big" + "github.com/8thlight/vulcanizedb/cmd" "github.com/8thlight/vulcanizedb/pkg/geth" "github.com/8thlight/vulcanizedb/pkg/watched_contracts" @@ -15,14 +17,27 @@ import ( func main() { environment := flag.String("environment", "", "Environment name") contractHash := flag.String("contract-hash", "", "Contract hash to show summary") + _blockNumber := flag.Int64("block-number", -1, "Block number of summary") flag.Parse() config := cmd.LoadConfig(*environment) blockchain := geth.NewGethBlockchain(config.Client.IPCPath) repository := cmd.LoadPostgres(config.Database) - contractSummary, err := watched_contracts.NewSummary(blockchain, repository, *contractHash) + blockNumber := requestedBlockNumber(_blockNumber) + + contractSummary, err := watched_contracts.NewSummary(blockchain, repository, *contractHash, blockNumber) if err != nil { log.Fatalln(err) } output := watched_contracts.GenerateConsoleOutput(contractSummary) fmt.Println(output) } + +func requestedBlockNumber(blockNumber *int64) *big.Int { + var _blockNumber *big.Int + if *blockNumber == -1 { + _blockNumber = nil + } else { + _blockNumber = big.NewInt(*blockNumber) + } + return _blockNumber +} diff --git a/integration_test/geth_blockchain_test.go b/integration_test/geth_blockchain_test.go index 2455f79a..8a9336c9 100644 --- a/integration_test/geth_blockchain_test.go +++ b/integration_test/geth_blockchain_test.go @@ -42,7 +42,7 @@ var _ = Describe("Reading from the Geth blockchain", func() { Expect(firstBlock.Number + 1).Should(Equal(secondBlock.Number)) close(done) - }, 10) + }, 15) It("retrieves the genesis block and first block", func(done Done) { genesisBlock := blockchain.GetBlockByNumber(int64(0)) @@ -52,6 +52,6 @@ var _ = Describe("Reading from the Geth blockchain", func() { Expect(firstBlock.Number).To(Equal(int64(1))) close(done) - }, 10) + }, 15) }) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 50c07f00..a80af9e8 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1,10 +1,12 @@ package core +import "math/big" + type Blockchain interface { GetBlockByNumber(blockNumber int64) Block SubscribeToBlocks(blocks chan Block) StartListening() StopListening() GetContract(contractHash string) (Contract, error) - GetAttribute(contract Contract, attributeName string) (interface{}, error) + GetAttribute(contract Contract, attributeName string, blockNumber *big.Int) (interface{}, error) } diff --git a/pkg/fakes/blockchain.go b/pkg/fakes/blockchain.go index edcca937..48ec5070 100644 --- a/pkg/fakes/blockchain.go +++ b/pkg/fakes/blockchain.go @@ -3,6 +3,8 @@ package fakes import ( "sort" + "math/big" + "github.com/8thlight/vulcanizedb/pkg/core" ) @@ -22,8 +24,13 @@ func (blockchain *Blockchain) GetContract(contractHash string) (core.Contract, e return contract, err } -func (blockchain *Blockchain) GetAttribute(contract core.Contract, attributeName string) (interface{}, error) { - result := blockchain.contractAttributes[contract.Hash][attributeName] +func (blockchain *Blockchain) GetAttribute(contract core.Contract, attributeName string, blockNumber *big.Int) (interface{}, error) { + var result interface{} + if blockNumber == nil { + result = blockchain.contractAttributes[contract.Hash+"-1"][attributeName] + } else { + result = blockchain.contractAttributes[contract.Hash+blockNumber.String()][attributeName] + } return result, nil } @@ -63,17 +70,23 @@ func (blockchain *Blockchain) StopListening() { blockchain.WasToldToStop = true } -func (blockchain *Blockchain) SetContractStateAttribute(contractHash string, attributeName string, attributeValue string) { - contractStateAttributes := blockchain.contractAttributes[contractHash] - if contractStateAttributes == nil { - blockchain.contractAttributes[contractHash] = make(map[string]string) +func (blockchain *Blockchain) SetContractStateAttribute(contractHash string, blockNumber *big.Int, attributeName string, attributeValue string) { + var key string + if blockNumber == nil { + key = contractHash + "-1" + } else { + key = contractHash + blockNumber.String() } - blockchain.contractAttributes[contractHash][attributeName] = attributeValue + contractStateAttributes := blockchain.contractAttributes[key] + if contractStateAttributes == nil { + blockchain.contractAttributes[key] = make(map[string]string) + } + blockchain.contractAttributes[key][attributeName] = attributeValue } func (blockchain *Blockchain) getContractAttributes(contractHash string) (core.ContractAttributes, error) { var contractAttributes core.ContractAttributes - attributes, ok := blockchain.contractAttributes[contractHash] + attributes, ok := blockchain.contractAttributes[contractHash+"-1"] if ok { for key, _ := range attributes { contractAttributes = append(contractAttributes, core.ContractAttribute{Name: key, Type: "string"}) diff --git a/pkg/geth/contract.go b/pkg/geth/contract.go index fd6afdea..3c3f812c 100644 --- a/pkg/geth/contract.go +++ b/pkg/geth/contract.go @@ -7,9 +7,13 @@ import ( "sort" + "context" + "math/big" + "github.com/8thlight/vulcanizedb/pkg/config" "github.com/8thlight/vulcanizedb/pkg/core" - "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" ) @@ -30,18 +34,40 @@ func (blockchain *GethBlockchain) GetContract(contractHash string) (core.Contrac } } -func (blockchain *GethBlockchain) GetAttribute(contract core.Contract, attributeName string) (interface{}, error) { - boundContract, err := bindContract(common.HexToAddress(contract.Hash), blockchain.client, blockchain.client) +func (blockchain *GethBlockchain) getParseAbi(contract core.Contract) (abi.ABI, error) { + abiFilePath := filepath.Join(config.ProjectRoot(), "contracts", "public", fmt.Sprintf("%s.json", contract.Hash)) + parsed, err := ParseAbiFile(abiFilePath) + if err != nil { + return abi.ABI{}, err + } + return parsed, nil +} + +func (blockchain *GethBlockchain) GetAttribute(contract core.Contract, attributeName string, blockNumber *big.Int) (interface{}, error) { + parsed, err := blockchain.getParseAbi(contract) + var result interface{} + if err != nil { + return result, err + } + input, err := parsed.Pack(attributeName) if err != nil { return nil, err } - var result interface{} - err = boundContract.Call(&bind.CallOpts{}, &result, attributeName) + output, err := callContract(contract, input, err, blockchain, blockNumber) + if err != nil { + return nil, err + } + err = parsed.Unpack(&result, attributeName, output) if err != nil { return nil, ErrInvalidStateAttribute } return result, nil } +func callContract(contract core.Contract, input []byte, err error, blockchain *GethBlockchain, blockNumber *big.Int) ([]byte, error) { + to := common.HexToAddress(contract.Hash) + msg := ethereum.CallMsg{To: &to, Data: input} + return blockchain.client.CallContract(context.Background(), msg, blockNumber) +} func (blockchain *GethBlockchain) getContractAttributes(contractHash string) (core.ContractAttributes, error) { abiFilePath := filepath.Join(config.ProjectRoot(), "contracts", "public", fmt.Sprintf("%s.json", contractHash)) @@ -56,12 +82,3 @@ func (blockchain *GethBlockchain) getContractAttributes(contractHash string) (co sort.Sort(contractAttributes) return contractAttributes, nil } - -func bindContract(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor) (bind.BoundContract, error) { - abiFilePath := filepath.Join(config.ProjectRoot(), "contracts", "public", fmt.Sprintf("%s.json", address.Hex())) - parsed, err := ParseAbiFile(abiFilePath) - if err != nil { - return bind.BoundContract{}, err - } - return *bind.NewBoundContract(address, parsed, caller, transactor), nil -} diff --git a/pkg/geth/contract_test.go b/pkg/geth/contract_test.go index 2bbb5db7..3f4a6637 100644 --- a/pkg/geth/contract_test.go +++ b/pkg/geth/contract_test.go @@ -1,6 +1,8 @@ package geth_test //import ( +// "math/big" +// // cfg "github.com/8thlight/vulcanizedb/pkg/config" // "github.com/8thlight/vulcanizedb/pkg/geth" // "github.com/8thlight/vulcanizedb/pkg/geth/testing" @@ -57,19 +59,43 @@ package geth_test // contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" // // contract, _ := blockchain.GetContract(contractHash) -// name, err := blockchain.GetAttribute(contract, "name") +// name, err := blockchain.GetAttribute(contract, "name", nil) // // Expect(err).To(BeNil()) // Expect(name).To(Equal("OMGToken")) // }) // +// It("returns the correct attribute for a real contract", func() { +// config, _ := cfg.NewConfig("public") +// blockchain := geth.NewGethBlockchain(config.Client.IPCPath) +// contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" +// +// contract, _ := blockchain.GetContract(contractHash) +// name, err := blockchain.GetAttribute(contract, "name", nil) +// +// Expect(err).To(BeNil()) +// Expect(name).To(Equal("OMGToken")) +// }) +// +// It("returns the correct attribute for a real contract at a specific block height", func() { +// config, _ := cfg.NewConfig("public") +// blockchain := geth.NewGethBlockchain(config.Client.IPCPath) +// contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" +// +// contract, _ := blockchain.GetContract(contractHash) +// name, err := blockchain.GetAttribute(contract, "name", big.NewInt(4652791)) +// +// Expect(name).To(Equal("OMGToken")) +// Expect(err).To(BeNil()) +// }) +// // It("returns an error when there is no ABI for the given contract", func() { // config, _ := cfg.NewConfig("public") // blockchain := geth.NewGethBlockchain(config.Client.IPCPath) // contractHash := "MISSINGHASH" // // contract, _ := blockchain.GetContract(contractHash) -// name, err := blockchain.GetAttribute(contract, "name") +// name, err := blockchain.GetAttribute(contract, "name", nil) // // Expect(err).To(Equal(geth.ErrMissingAbiFile)) // Expect(name).To(BeNil()) @@ -81,7 +107,7 @@ package geth_test // contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" // // contract, _ := blockchain.GetContract(contractHash) -// name, err := blockchain.GetAttribute(contract, "missing_attribute") +// name, err := blockchain.GetAttribute(contract, "missing_attribute", nil) // // Expect(err).To(Equal(geth.ErrInvalidStateAttribute)) // Expect(name).To(BeNil()) diff --git a/pkg/watched_contracts/summary.go b/pkg/watched_contracts/summary.go index 22a2c0c2..71bf112d 100644 --- a/pkg/watched_contracts/summary.go +++ b/pkg/watched_contracts/summary.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" + "math/big" + "github.com/8thlight/vulcanizedb/pkg/core" "github.com/8thlight/vulcanizedb/pkg/repositories" ) @@ -15,27 +17,29 @@ type ContractSummary struct { LastTransaction *core.Transaction blockChain core.Blockchain Attributes core.ContractAttributes + BlockNumber *big.Int } var NewContractNotWatchedErr = func(contractHash string) error { return errors.New(fmt.Sprintf("Contract %v not being watched", contractHash)) } -func NewSummary(blockchain core.Blockchain, repository repositories.Repository, contractHash string) (ContractSummary, error) { +func NewSummary(blockchain core.Blockchain, repository repositories.Repository, contractHash string, blockNumber *big.Int) (ContractSummary, error) { watchedContract := repository.FindWatchedContract(contractHash) if watchedContract != nil { - return newContractSummary(blockchain, *watchedContract), nil + return newContractSummary(blockchain, *watchedContract, blockNumber), nil } else { return ContractSummary{}, NewContractNotWatchedErr(contractHash) } } func (contractSummary ContractSummary) GetStateAttribute(attributeName string) interface{} { - result, _ := contractSummary.blockChain.GetAttribute(contractSummary.Contract, attributeName) + var result interface{} + result, _ = contractSummary.blockChain.GetAttribute(contractSummary.Contract, attributeName, contractSummary.BlockNumber) return result } -func newContractSummary(blockchain core.Blockchain, watchedContract core.WatchedContract) ContractSummary { +func newContractSummary(blockchain core.Blockchain, watchedContract core.WatchedContract, blockNumber *big.Int) ContractSummary { contract, _ := blockchain.GetContract(watchedContract.Hash) return ContractSummary{ blockChain: blockchain, @@ -44,6 +48,7 @@ func newContractSummary(blockchain core.Blockchain, watchedContract core.Watched NumberOfTransactions: len(watchedContract.Transactions), LastTransaction: lastTransaction(watchedContract), Attributes: contract.Attributes, + BlockNumber: blockNumber, } } diff --git a/pkg/watched_contracts/summary_test.go b/pkg/watched_contracts/summary_test.go index c9dda6ba..8308ad03 100644 --- a/pkg/watched_contracts/summary_test.go +++ b/pkg/watched_contracts/summary_test.go @@ -1,6 +1,8 @@ package watched_contracts_test import ( + "math/big" + "github.com/8thlight/vulcanizedb/pkg/core" "github.com/8thlight/vulcanizedb/pkg/fakes" "github.com/8thlight/vulcanizedb/pkg/repositories" @@ -9,14 +11,18 @@ import ( . "github.com/onsi/gomega" ) -var _ bool = Describe("The watched contract summary", func() { +func NewCurrentContractSummary(blockchain core.Blockchain, repository repositories.Repository, contractHash string) (watched_contracts.ContractSummary, error) { + return watched_contracts.NewSummary(blockchain, repository, contractHash, nil) +} + +var _ = Describe("The watched contract summary", func() { Context("when the given contract is not being watched", func() { It("returns an error", func() { repository := repositories.NewInMemory() blockchain := fakes.NewBlockchain() - contractSummary, err := watched_contracts.NewSummary(blockchain, repository, "123") + contractSummary, err := NewCurrentContractSummary(blockchain, repository, "0x123") Expect(contractSummary).To(Equal(watched_contracts.ContractSummary{})) Expect(err).NotTo(BeNil()) @@ -30,7 +36,7 @@ var _ bool = Describe("The watched contract summary", func() { repository.CreateWatchedContract(watchedContract) blockchain := fakes.NewBlockchain() - contractSummary, err := watched_contracts.NewSummary(blockchain, repository, "0x123") + contractSummary, err := NewCurrentContractSummary(blockchain, repository, "0x123") Expect(contractSummary).NotTo(Equal(watched_contracts.ContractSummary{})) Expect(err).To(BeNil()) @@ -42,7 +48,7 @@ var _ bool = Describe("The watched contract summary", func() { repository.CreateWatchedContract(watchedContract) blockchain := fakes.NewBlockchain() - contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123") + contractSummary, _ := NewCurrentContractSummary(blockchain, repository, "0x123") Expect(contractSummary.ContractHash).To(Equal("0x123")) }) @@ -60,7 +66,7 @@ var _ bool = Describe("The watched contract summary", func() { repository.CreateBlock(block) blockchain := fakes.NewBlockchain() - contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123") + contractSummary, _ := NewCurrentContractSummary(blockchain, repository, "0x123") Expect(contractSummary.NumberOfTransactions).To(Equal(2)) }) @@ -78,7 +84,7 @@ var _ bool = Describe("The watched contract summary", func() { repository.CreateBlock(block) blockchain := fakes.NewBlockchain() - contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123") + contractSummary, _ := NewCurrentContractSummary(blockchain, repository, "0x123") Expect(contractSummary.LastTransaction.Hash).To(Equal("TRANSACTION2")) }) @@ -88,23 +94,38 @@ var _ bool = Describe("The watched contract summary", func() { watchedContract := core.WatchedContract{Hash: "0x123"} repository.CreateWatchedContract(watchedContract) blockchain := fakes.NewBlockchain() - blockchain.SetContractStateAttribute("0x123", "foo", "bar") + blockchain.SetContractStateAttribute("0x123", nil, "foo", "bar") - contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123") + contractSummary, _ := NewCurrentContractSummary(blockchain, repository, "0x123") attribute := contractSummary.GetStateAttribute("foo") Expect(attribute).To(Equal("bar")) }) + It("gets contract state attribute for the contract from the blockchain at specific block height", func() { + repository := repositories.NewInMemory() + watchedContract := core.WatchedContract{Hash: "0x123"} + repository.CreateWatchedContract(watchedContract) + blockchain := fakes.NewBlockchain() + blockNumber := big.NewInt(1000) + blockchain.SetContractStateAttribute("0x123", nil, "foo", "bar") + blockchain.SetContractStateAttribute("0x123", blockNumber, "foo", "baz") + + contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123", blockNumber) + attribute := contractSummary.GetStateAttribute("foo") + + Expect(attribute).To(Equal("baz")) + }) + It("gets attributes for the contract from the blockchain", func() { repository := repositories.NewInMemory() watchedContract := core.WatchedContract{Hash: "0x123"} repository.CreateWatchedContract(watchedContract) blockchain := fakes.NewBlockchain() - blockchain.SetContractStateAttribute("0x123", "foo", "bar") - blockchain.SetContractStateAttribute("0x123", "baz", "bar") + blockchain.SetContractStateAttribute("0x123", nil, "foo", "bar") + blockchain.SetContractStateAttribute("0x123", nil, "baz", "bar") - contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123") + contractSummary, _ := NewCurrentContractSummary(blockchain, repository, "0x123") Expect(contractSummary.Attributes).To(Equal( core.ContractAttributes{