diff --git a/db/migrations/00014_create_stored_functions.sql b/db/migrations/00014_create_stored_functions.sql index 1e9ad468..e0374636 100644 --- a/db/migrations/00014_create_stored_functions.sql +++ b/db/migrations/00014_create_stored_functions.sql @@ -1,4 +1,20 @@ -- +goose Up +-- +goose StatementBegin +-- returns if a state leaf node was removed within the provided block number +CREATE OR REPLACE FUNCTION was_state_leaf_removed(key character varying, hash character varying) + RETURNS boolean AS $$ + SELECT state_cids.node_type = 3 + FROM eth.state_cids + INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) + WHERE state_leaf_key = key + AND block_number <= (SELECT block_number + FROM eth.header_cids + WHERE block_hash = hash) + ORDER BY block_number DESC LIMIT 1; +$$ +language sql; +-- +goose StatementEnd + -- +goose StatementBegin CREATE TYPE child_result AS ( has_child BOOLEAN, @@ -115,6 +131,7 @@ LANGUAGE 'plpgsql'; -- +goose StatementEnd -- +goose Down +DROP FUNCTION was_state_leaf_removed; DROP FUNCTION canonical_header_id; DROP FUNCTION canonical_header_from_array; DROP FUNCTION has_child; diff --git a/docker-compose.yml b/docker-compose.yml index c24619bc..15c73bce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: restart: on-failure depends_on: - db - image: vulcanize/statediff-migrations:v0.7.0 + image: vulcanize/statediff-migrations:v0.8.0 environment: DATABASE_USER: vdbm DATABASE_NAME: vulcanize_public @@ -39,6 +39,7 @@ services: - vdb_db_eth_server:/var/lib/postgresql/data ports: - "127.0.0.1:8077:5432" + command: ["postgres", "-c", "log_statement=all"] eth-server: restart: unless-stopped @@ -61,6 +62,7 @@ services: DATABASE_USER: "vdbm" DATABASE_PASSWORD: "password" ETH_CHAIN_ID: 4 + RUN_DB_MIGRATION: "no" volumes: - type: bind source: ./chain.json diff --git a/entrypoint.sh b/entrypoint.sh index 2c9e9821..0efef13f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,18 +4,21 @@ # Construct the connection string for postgres VDB_PG_CONNECT=postgresql://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_HOSTNAME:$DATABASE_PORT/$DATABASE_NAME?sslmode=disable -# Run the DB migrations -echo "Connecting with: $VDB_PG_CONNECT" -echo "Running database migrations" -./goose -dir migrations/vulcanizedb postgres "$VDB_PG_CONNECT" up -rv=$? -if [ $rv != 0 ]; then - echo "Could not run migrations. Are the database details correct?" - exit 1 +if [ "$RUN_DB_MIGRATION" != "no" ] +then + # Run the DB migrations + echo "Connecting with: $VDB_PG_CONNECT" + echo "Running database migrations" + ./goose -dir migrations/vulcanizedb postgres "$VDB_PG_CONNECT" up + rv=$? + + if [ $rv != 0 ]; then + echo "Could not run migrations. Are the database details correct?" + exit 1 + fi fi - echo "Beginning the ipld-eth-server process" echo running: ./ipld-eth-server ${VDB_COMMAND} --config=config.toml diff --git a/pkg/eth/eth_state_test.go b/pkg/eth/eth_state_test.go index 8ee4a12f..6ff70c25 100644 --- a/pkg/eth/eth_state_test.go +++ b/pkg/eth/eth_state_test.go @@ -476,16 +476,16 @@ var _ = Describe("eth state reading tests", func() { It("Returns empty slice if it tries to access a contract which does not exist", func() { storage, err := api.GetStorageAt(ctx, test_helpers.ContractAddr, test_helpers.ContractSlotKeyHash.Hex(), rpc.BlockNumberOrHashWithNumber(0)) Expect(err).NotTo(HaveOccurred()) - Expect(storage).To(Equal(hexutil.Bytes(make([]byte, 32)))) + Expect(storage).To(Equal(hexutil.Bytes(eth.EmptyNodeValue))) storage, err = api.GetStorageAt(ctx, test_helpers.ContractAddr, test_helpers.ContractSlotKeyHash.Hex(), rpc.BlockNumberOrHashWithNumber(1)) Expect(err).NotTo(HaveOccurred()) - Expect(storage).To(Equal(hexutil.Bytes(make([]byte, 32)))) + Expect(storage).To(Equal(hexutil.Bytes(eth.EmptyNodeValue))) }) It("Returns empty slice if it tries to access a contract slot which does not exist", func() { storage, err := api.GetStorageAt(ctx, test_helpers.ContractAddr, randomHash.Hex(), rpc.BlockNumberOrHashWithNumber(2)) Expect(err).NotTo(HaveOccurred()) - Expect(storage).To(Equal(hexutil.Bytes(make([]byte, 32)))) + Expect(storage).To(Equal(hexutil.Bytes(eth.EmptyNodeValue))) }) It("Retrieves the storage value at the provided contract address and storage leaf key at the block with the provided hash or number", func() { // After deployment @@ -506,7 +506,7 @@ var _ = Describe("eth state reading tests", func() { val, err = api.GetStorageAt(ctx, test_helpers.ContractAddr, test_helpers.IndexOne, rpc.BlockNumberOrHashWithNumber(5)) Expect(err).ToNot(HaveOccurred()) - Expect(val).To(Equal(hexutil.Bytes{})) + Expect(val).To(Equal(hexutil.Bytes(eth.EmptyNodeValue))) }) It("Throws an error for a non-existing block hash", func() { _, err := api.GetStorageAt(ctx, test_helpers.ContractAddr, test_helpers.IndexOne, rpc.BlockNumberOrHashWithHash(randomHash, true)) diff --git a/pkg/eth/ipld_retriever.go b/pkg/eth/ipld_retriever.go index 38f1b0ae..08e11884 100644 --- a/pkg/eth/ipld_retriever.go +++ b/pkg/eth/ipld_retriever.go @@ -127,7 +127,7 @@ const ( AND block_number <= $2 ORDER BY block_number DESC LIMIT 1` - RetrieveStorageLeafByAddressHashAndLeafKeyAndBlockNumberPgStr = `SELECT storage_cids.cid, data, storage_cids.node_type + RetrieveStorageLeafByAddressHashAndLeafKeyAndBlockNumberPgStr = `SELECT storage_cids.cid, data, storage_cids.node_type, was_state_leaf_removed($1, $3) AS state_leaf_removed FROM eth.storage_cids INNER JOIN eth.state_cids ON (storage_cids.state_id = state_cids.id) INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) @@ -137,7 +137,7 @@ const ( AND block_number <= $3 ORDER BY block_number DESC LIMIT 1` - RetrieveStorageLeafByAddressHashAndLeafKeyAndBlockHashPgStr = `SELECT storage_cids.cid, data, storage_cids.node_type + RetrieveStorageLeafByAddressHashAndLeafKeyAndBlockHashPgStr = `SELECT storage_cids.cid, data, storage_cids.node_type, was_state_leaf_removed($1, $3) AS state_leaf_removed FROM eth.storage_cids INNER JOIN eth.state_cids ON (storage_cids.state_id = state_cids.id) INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) @@ -152,6 +152,8 @@ const ( LIMIT 1` ) +var EmptyNodeValue = make([]byte, common.HashLength) + type rctIpldResult struct { LeafCID string `db:"leaf_cid"` Data []byte `db:"data"` @@ -425,9 +427,10 @@ func (r *IPLDRetriever) RetrieveReceiptByHash(hash common.Hash) (string, []byte, } type nodeInfo struct { - CID string `db:"cid"` - Data []byte `db:"data"` - NodeType int `db:"node_type"` + CID string `db:"cid"` + Data []byte `db:"data"` + NodeType int `db:"node_type"` + StateLeafRemoved bool `db:"state_leaf_removed"` } // RetrieveAccountByAddressAndBlockHash returns the cid and rlp bytes for the account corresponding to the provided address and block hash @@ -438,9 +441,11 @@ func (r *IPLDRetriever) RetrieveAccountByAddressAndBlockHash(address common.Addr if err := r.db.Get(accountResult, RetrieveAccountByLeafKeyAndBlockHashPgStr, leafKey.Hex(), hash.Hex()); err != nil { return "", nil, err } + if accountResult.NodeType == removedNode { - return "", []byte{}, nil + return "", EmptyNodeValue, nil } + var i []interface{} if err := rlp.DecodeBytes(accountResult.Data, &i); err != nil { return "", nil, fmt.Errorf("error decoding state leaf node rlp: %s", err.Error()) @@ -459,9 +464,11 @@ func (r *IPLDRetriever) RetrieveAccountByAddressAndBlockNumber(address common.Ad if err := r.db.Get(accountResult, RetrieveAccountByLeafKeyAndBlockNumberPgStr, leafKey.Hex(), number); err != nil { return "", nil, err } + if accountResult.NodeType == removedNode { - return "", []byte{}, nil + return "", EmptyNodeValue, nil } + var i []interface{} if err := rlp.DecodeBytes(accountResult.Data, &i); err != nil { return "", nil, fmt.Errorf("error decoding state leaf node rlp: %s", err.Error()) @@ -480,8 +487,8 @@ func (r *IPLDRetriever) RetrieveStorageAtByAddressAndStorageSlotAndBlockHash(add if err := r.db.Get(storageResult, RetrieveStorageLeafByAddressHashAndLeafKeyAndBlockHashPgStr, stateLeafKey.Hex(), storageHash.Hex(), hash.Hex()); err != nil { return "", nil, nil, err } - if storageResult.NodeType == removedNode { - return "", []byte{}, []byte{}, nil + if storageResult.StateLeafRemoved || storageResult.NodeType == removedNode { + return "", EmptyNodeValue, EmptyNodeValue, nil } var i []interface{} if err := rlp.DecodeBytes(storageResult.Data, &i); err != nil { @@ -502,8 +509,9 @@ func (r *IPLDRetriever) RetrieveStorageAtByAddressAndStorageKeyAndBlockNumber(ad if err := r.db.Get(storageResult, RetrieveStorageLeafByAddressHashAndLeafKeyAndBlockNumberPgStr, stateLeafKey.Hex(), storageLeafKey.Hex(), number); err != nil { return "", nil, err } - if storageResult.NodeType == removedNode { - return "", []byte{}, nil + + if storageResult.StateLeafRemoved || storageResult.NodeType == removedNode { + return "", EmptyNodeValue, nil } var i []interface{} if err := rlp.DecodeBytes(storageResult.Data, &i); err != nil { diff --git a/pkg/graphql/graphql.go b/pkg/graphql/graphql.go index d12838e2..1f36ef33 100644 --- a/pkg/graphql/graphql.go +++ b/pkg/graphql/graphql.go @@ -18,6 +18,7 @@ package graphql import ( + "bytes" "context" "database/sql" "errors" @@ -31,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/vulcanize/ipld-eth-server/pkg/eth" ) @@ -1011,6 +1013,10 @@ func (r *Resolver) GetStorageAt(ctx context.Context, args struct { return nil, err } + if bytes.Compare(rlpValue, eth.EmptyNodeValue) == 0 { + return &StorageResult{value: eth.EmptyNodeValue, cid: cid, ipldBlock: ipldBlock}, nil + } + var value interface{} err = rlp.DecodeBytes(rlpValue, &value) if err != nil { diff --git a/scripts/run_intregration_test.sh b/scripts/run_intregration_test.sh index f9056dc4..929e6297 100755 --- a/scripts/run_intregration_test.sh +++ b/scripts/run_intregration_test.sh @@ -5,8 +5,8 @@ set -o xtrace docker-compose down --remove-orphans --volumes # Build and start the containers. -# Note: Build only if `ipld-eth-server` code is modified. Otherwise comment this line. -docker build -t ipld-eth-server_eth-server:latest . +# Note: Build only if `ipld-eth-server` or other container code is modified. Otherwise comment this line. +docker-compose -f docker-compose.test.yml -f docker-compose.yml build eth-server docker-compose -f docker-compose.test.yml -f docker-compose.yml up -d db dapptools contract eth-server export PGPASSWORD=password diff --git a/test/contract/contracts/GLDToken.sol b/test/contract/contracts/GLDToken.sol index 4cd3205f..596baad3 100644 --- a/test/contract/contracts/GLDToken.sol +++ b/test/contract/contracts/GLDToken.sol @@ -4,4 +4,7 @@ contract GLDToken is ERC20 { constructor() ERC20("Gold", "GLD") { _mint(msg.sender, 1000000000000000000000); } + function destroy() public { + selfdestruct(payable(msg.sender)); + } } diff --git a/test/contract/src/index.js b/test/contract/src/index.js index 37639542..b20d54a2 100644 --- a/test/contract/src/index.js +++ b/test/contract/src/index.js @@ -1,6 +1,7 @@ const fastify = require('fastify')({ logger: true }); const hre = require("hardhat"); + // readiness check fastify.get('/v1/healthz', async (req, reply) => { reply @@ -22,6 +23,20 @@ fastify.get('/v1/deployContract', async (req, reply) => { } }); +fastify.get('/v1/destroyContract', async (req, reply) => { + const addr = req.query.addr; + + const Token = await hre.ethers.getContractFactory("GLDToken"); + const token = await Token.attach(addr); + + await token.destroy(); + const blockNum = await hre.ethers.provider.getBlockNumber() + + return { + blockNumber: blockNum, + } +}) + fastify.get('/v1/sendEth', async (req, reply) => { const to = req.query.to; const value = req.query.value; diff --git a/test/helper.go b/test/helper.go index e458bfde..5cd8ee2a 100644 --- a/test/helper.go +++ b/test/helper.go @@ -14,6 +14,10 @@ type ContractDeployed struct { BlockHash string `json:"blockHash"` } +type ContractDestroyed struct { + BlockNumber int64 `json:"blockNumber"` +} + type Tx struct { From string `json:"from"` To string `json:"to"` @@ -43,6 +47,19 @@ func DeployContract() (*ContractDeployed, error) { return &contract, nil } +func DestroyContract(addr string) (*ContractDestroyed, error) { + res, err := http.Get(fmt.Sprintf("%s/v1/destroyContract?addr=%s", srvUrl, addr)) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data ContractDestroyed + decoder := json.NewDecoder(res.Body) + + return &data, decoder.Decode(&data) +} + func SendEth(to string, value string) (*Tx, error) { res, err := http.Get(fmt.Sprintf("%s/v1/sendEth?to=%s&value=%s", srvUrl, to, value)) if err != nil { diff --git a/test/integration_test.go b/test/integration_test.go index 43cb8626..d250fcce 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -9,12 +9,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - integration "github.com/vulcanize/ipld-eth-server/test" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/vulcanize/ipld-eth-server/pkg/eth" + integration "github.com/vulcanize/ipld-eth-server/test" ) const nonExistingBlockHash = "0x111111111111111111111111111111111111111111111111111111111111111" @@ -391,6 +392,37 @@ var _ = Describe("Integration test", func() { Expect(err).To(MatchError("header not found")) Expect(gethStorage).To(Equal(ipldStorage)) }) + + It("get storage after self destruct", func() { + totalSupplyIndex := "0x2" + + tx, err := integration.DestroyContract(contract.Address) + Expect(err).ToNot(HaveOccurred()) + + time.Sleep(sleepInterval) + + gethStorage1, err := gethClient.StorageAt(ctx, common.HexToAddress(contract.Address), common.HexToHash(totalSupplyIndex), big.NewInt(tx.BlockNumber-1)) + Expect(err).ToNot(HaveOccurred()) + gethStorage2, err := gethClient.StorageAt(ctx, common.HexToAddress(contract.Address), common.HexToHash(totalSupplyIndex), big.NewInt(tx.BlockNumber)) + Expect(err).ToNot(HaveOccurred()) + + Expect(gethStorage1).NotTo(Equal(gethStorage2)) + Expect(gethStorage2).To(Equal(eth.EmptyNodeValue)) + + ipldStorage1, err := ipldClient.StorageAt(ctx, common.HexToAddress(contract.Address), common.HexToHash(totalSupplyIndex), big.NewInt(tx.BlockNumber-1)) + Expect(err).ToNot(HaveOccurred()) + ipldStorage2, err := ipldClient.StorageAt(ctx, common.HexToAddress(contract.Address), common.HexToHash(totalSupplyIndex), big.NewInt(tx.BlockNumber)) + Expect(err).ToNot(HaveOccurred()) + + Expect(ipldStorage1).To(Equal(gethStorage1)) + Expect(ipldStorage2).To(Equal(gethStorage2)) + + // Query the current block + ipldStorage3, err := ipldClient.StorageAt(ctx, common.HexToAddress(contract.Address), common.HexToHash(totalSupplyIndex), nil) + Expect(err).ToNot(HaveOccurred()) + + Expect(ipldStorage2).To(Equal(ipldStorage3)) + }) }) Describe("eth call", func() {