web3 / web3.js

Collection of comprehensive TypeScript libraries for Interaction with the Ethereum JSON RPC API and utility functions.
https://web3js.org/
Other
19.35k stars 4.96k forks source link

WebsocketProvider with `fromBlock` doesnt return events from the past #3389

Closed VladoDemcak closed 4 years ago

VladoDemcak commented 4 years ago

Maybe I am facing the same issue as described here: https://github.com/ethereum/web3.js/issues/3379.

Current behaviour When I am subscribed I get only events from last block. Eg if there are events in block 0,1,2,3 and I set fromBlock: 0 and the current block is 3 I get only events from block 3 and not from 0.

Expected behaviour Once I am subscribed and I specify fromBlock: 0, I should get all events from block 0, 1 ... latest.

I've created the post on stackoverflow as well so more details about the problem I am facing is described here.

I am using WebsocketProvider and quorum-v2.4.0 and web3js 1.2.6. So I am connected like this:

const web3 = new Web3("ws://127.0.0.1:23000"); 

When I create subscription for my contract it looks like below. I specify fromBlock because I want to get all events from block 0 when I am creating subscription.

 contract.events.Transfer({
    fromBlock: 0,
  })
    .on("connected", function (x) {
      // do somethinng
    })
    .on('data', function (x) {
      // do somethinng
    })
    .on('error', function (x) {
      // do somethinng
    });

However it doesn't return events from block 0 even though there are some events. It returns only the events from last block. So if current block is 10 and there was an event in block 9 it returns only this event but doesnt return event from block 8,7,6...0.

After some time of investigating the issue I have found there is a weird part of the implementation in subscription.js where fromBlock is removed with delete keyword. Check the snipped from subscription.js below.

    // create subscription
    // TODO move to separate function? so that past logs can go first?

    if(typeof payload.params[1] === 'object')
        delete payload.params[1].fromBlock;

Since this code deletes thefromBlock from params it is missing in WebsocketProvider.prototype.send.

Because of this delete part the payload in send function in WebsocketProvider doesnt call RPC with fromBlock parameter (request is without fromBlock) and I assume it is considered as latest so it doesn't return events from past:

After deleting fromBlock param the request looks like this:

{
   "jsonrpc":"2.0",
   "id":1,
   "method":"eth_getLogs",
   "params":[
      {
         "topics":[
            "0xddf2....",
            null,
            null
         ],
         "address":"0x9d13...."
      }
   ]
}

When I comment out the part where fromBlock is being deleted the payload is:

{
   "jsonrpc":"2.0",
   "id":1,
   "method":"eth_getLogs",
   "params":[
      {
         "fromBlock":"0x0",
         "topics":[
            "0xdd...",
            null,
            null
         ],
         "address":"0x9d..."
      }
   ]
}

and this second example correctly returns the events from block 0 not only the new.

VladoDemcak commented 4 years ago

@frozeman - Could you help me with this please?

I found your commit from 2016 :D where the part with deleting fromBlock from params in subscription has been added. I still dont have clue why it's here. When I comment it out the subscription for events takes fromBlock correctly. otherwise it takes only the latest block.

Here is the commit: https://github.com/ethereum/web3.js/commit/23f428b09627095658d09d306455082641424aac

cgewecke commented 4 years ago

@VladoDemcak Thanks for opening and investigating - will look at this today along with #3379.

cgewecke commented 4 years ago

@VladoDemcak I've tried to build a reproduction for this in #3391 but it looks like everything works...

The case replicates what you've described here, (using Geth stable (1.9), mining at 2 second intervals):

So if current block is 10 and there was an event in block 9 it returns only this event but doesnt return event from block 8,7,6...0.

The Travis CI output for it is here.

Does that example +/- model your request correctly?

teawaterwire commented 4 years ago

i also have this issue of the fromBlock being ignored when calling contract.events.Transfer (weirdly enough it's on the "Transfer" event of an ERC20 contract)

in my case i'm connected to the Portis provider on Rinkeby, but i'm passing a filter parameter (maybe the one difference with your test @cgewecke ?)

VladoDemcak commented 4 years ago

@cgewecke Thanks for your effort. Is your test case isolated from the others? Could you please retest the issue with steps on kovan network I am describing below?

I've tested it on kovan network and it doesn't return any events from the past when I create subscription for Transfer events.

I deployed my custom SimpleToken on address 0xaA8f1B72Ac1c9769D25a9251A982fb4a6c296Bf1

Check the txns for the token: image

There are several transactions in multiple blocks eg: 1699289, 16992925, 16993448 ... (at least 6 blocks)

I have found very interesting behavior please see below:

Steps to reproduce the issue:

  1. create .js file with content of example.js from below

  2. Change INFURA_KEY to your ProjectID. How to register project id is described below: 2.1 Go to: https://infura.io/register 2.2 Create new account 2.3 "CREATE NEW PROJECT", give it a nam, copy PROJECT_ID and replace INFURA_KEY with your PROJECT_ID in example.js image

  3. Run the example.js -> it should print :

    listening...

    and nothing else

  4. Now the funny part. Uncomment the code under // uncomment code below in example.js for getting results from the past. It doesn't print anything it's just simple web3 call

  5. Run the code again -> - it prints all events fromBlock -> event events from the PAST! and the fromBlock here works!!!

example.js:

const Web3 = require('web3');
const abi = [
  {
    "inputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "constant": true,
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "subtractedValue",
        "type": "uint256"
      }
    ],
    "name": "decreaseAllowance",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "addedValue",
        "type": "uint256"
      }
    ],
    "name": "increaseAllowance",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

const web3 = new Web3("wss://kovan.infura.io/ws/v3/INFURA_KEY");

const run = async () => {

  const contract = new web3.eth.Contract(abi, '0xaA8f1B72Ac1c9769D25a9251A982fb4a6c296Bf1');

  // uncomment code below 
  // await contract.getPastEvents('Transfer', {
  //   fromBlock:  0,
  //   toBlock: 'latest'
  // });

  contract.events.Transfer({fromBlock: 0})
    .on('data', (event) => console.log(event));

}

run()
  .then(() => {
    console.log("listening...")
  });
VladoDemcak commented 4 years ago

I observe this problem on:

cgewecke commented 4 years ago

@VladoDemcak Are you able to provide a super simple example using just a node.js script and ganache (or as a mocha test)? Something similar to the tests we have in the test suite here? #3391 also worked for ganache on my end...

Infura's websockets are known to be quite fragile. There are lots of dropped connections and sometimes swallowed errors. (A PR is open at #3190 to try to remedy this in several ways).

Tbh, if you're interested in storing events for a deployed contract via Infura the most robust solution is likely to set up an interval poll, and run getPastEvents every so often with an http provider.

VladoDemcak commented 4 years ago

@cgewecke This is super weird.

I will try to create some super simple example but everything is in const run = async () => { in my example from above comment.

Could you please verify/answer following:

  1. Do you have fresh, clean and new workspace in ganache?
  2. Do you have new ERC20 Token deployed?
  3. Do you call contract.getPastEvents('Transfer' or something else before subscribing to contract.events.Transfer({fromBlock: 0})?

I found it works when contract.getPastEvents('Transfer' is executed before subscribing.

cgewecke commented 4 years ago

@VladoDemcak I haven't run a transfer specific example because the issue you've raised is about fromBlock which should work the same for any event.

One way forward might be to express this problem in terms of Web3's E2E tests which try to be as minimal as possible...then work backwards to your specific case and identify the differences.

You should be able to clone Web3, install and run the E2E client tests using the commands listed in the main package.json's scripts

For example:

VladoDemcak commented 4 years ago

@cgewecke Thanks.

yes I saw the events tests and I am trying to run it locally.

my current suspicion is that in events tests the Basic SmartContract is being deployed via web3 and tests hold the instance

instance = await basic.deploy().send({from: accounts[0]});

In my case I deploy my contract via truffle and the instance is created based on abi and contract_address.

const contract = new web3.eth.Contract(abi, '0xaA8f1B72Ac1c9769D25a9251A982fb4a6c296Bf1');
VladoDemcak commented 4 years ago

@cgewecke

I've prepared repo where you should be able to replicate the problem. Please follow instructions in README. https://github.com/VladoDemcak/eth-subscribe-debug I also added screenshots of 3 terminals where you can see what was going on.

I think your tests work because there is the instance of contract which is used also for subscribing. And this instance has some attributes (or is in different state) which is necessary for subscription.

Important thing is that I observe the issue with fromBlock when I run subscription in independent node (the same case for Infura example I posted above).

The interesting part is that when I execute getPastEvents or send the Event before subscribing for Events in the same node, it works for every blockchain I tested on!!!

It looks like there is a hidden implementation in getPastEvents (or in sending event) which is adding some attribute to contract or establishes a connection and this affects behavior of "only subscription" approach.

On this link you can see 3 JSON.stringify(contract) outputs I have taken from listen.js in my repo example.

There are several differences between the contract objects. E.g working contracts (with prefix "working" in this gist ) are different in fields like: base64nonce, headers.sec-websocket-accept, maskBytes.data[] sooo nothing special.

BUT when I compare non-working contract with working contract there are differences in fields: _provider.connected: false in non-working contract and _provider.connected: true in working contract.

Next .. _requestManager.provider.connected: false in non-working example but true in working example. Again _readyState: 0 in non-working _readyState: 1 in working example .... and more ...

I will try to find more .... But please if you know how to fix this (e.g with some attribute or something) please let me know.

==========

edit:

even when i execute:

await web3.eth.getAccounts()

or

await web3.eth.getBlockNumber()

before calling subscription with fromBlock it works.

VladoDemcak commented 4 years ago

i also have this issue of the fromBlock being ignored when calling contract.events.Transfer (weirdly enough it's on the "Transfer" event of an ERC20 contract)

in my case i'm connected to the Portis provider on Rinkeby, but i'm passing a filter parameter (maybe the one difference with your test @cgewecke ?)

@teawaterwire Can you share your code?

cgewecke commented 4 years ago

await web3.eth.getAccounts()

Ai!! That's really weird. Thanks for making the repo, will take a look tomorrow and see if anything jumps out.

VladoDemcak commented 4 years ago

@cgewecke were you able to replicate the issue on your side?

cgewecke commented 4 years ago

@VladoDemcak Yes I was, thanks for making such a great reproduction.

Still investigating and your suggestion about whether the socket is connected seems like it would make sense.

However, I'm seeing the initial request for eth_getLogs here execute correctly in the "non-working" example. It just returns fewer logs in ganache, and no logs on geth unless a previous call has been made.

In sum, you cannot initiate an event subscription as the first communication you have with the client and get all the past logs as expected.

Very mysterious, especially the different responses from the client...smh.

VladoDemcak commented 4 years ago

In sum, you cannot initiate an event subscription as the first communication you have with the client and get all the past logs as expected.

Exactly! Thanks for re-testing.

If you will find any solution let me know and I'll test it. Meanwhile I will try to do something as well.

cgewecke commented 4 years ago

@VladoDemcak When I wrap the delete block statement you initially flagged as a problem in a 150ms timeout (here)

https://github.com/ethereum/web3.js/blob/ccc229e03edf805498d67ecbacbf69b0cec82e56/packages/web3-core-subscriptions/src/subscription.js#L246-L247

...it works.

It must be a race condition caused by latency setting up the initial Websocket connection. By the time the first request gets made, the param's been deleted.

holgerd77 commented 4 years ago

@cgewecke Thanks for this in-depth analysis, great 😄, will close here.

VladoDemcak commented 4 years ago

@holgerd77 why did you close the issue? Are you guys going to fix it somewhere?

holgerd77 commented 4 years ago

@VladoDemcak Sorry, I might have misread the last comment from @cgewecke, will re-open for now.

VladoDemcak commented 4 years ago

@holgerd77 @cgewecke Guys, I honestly appreciate all your effort you put into this. Do you have any plans to fix it any time soon?

I am asking if we should stay with the getAccounts() / getBlockNumber() / getGasPrice() workaround before creating an event subscription or you have a solution for this problem and we will wait for that fix?

cgewecke commented 4 years ago

@VladoDemcak I will open a PR with a fix today, but it may be a few weeks before Web3 is published again. Would probably use the work-around for the time being, sorry.

VladoDemcak commented 4 years ago

@cgewecke @holgerd77 if i may ask I would have one question which is not exactly related to the issue but cannot find any answer.

When I create subscription for e.g contract Transfer event like below.

For Example.

contract.events.Transfer({
    fromBlock: 0,
    transactionConfirmationBlocks: 5 <--- something like this line - according to doc I know there isn't something like this for subscription
  })

Can I get the event ONLY after 5 confirmations? Is there any way how to achieve this functionality without creating custom queue for received events and computing number of confirmation blocks?