Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebsocketProvider with fromBlock doesnt return events from the past #3389

Closed
VladoDemcak opened this issue Feb 24, 2020 · 23 comments · Fixed by #3455
Closed

WebsocketProvider with fromBlock doesnt return events from the past #3389

VladoDemcak opened this issue Feb 24, 2020 · 23 comments · Fixed by #3455
Labels
1.x 1.0 related issues Bug Addressing a bug

Comments

@VladoDemcak
Copy link
Contributor

VladoDemcak commented Feb 24, 2020

Maybe I am facing the same issue as described here: #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
Copy link
Contributor Author

VladoDemcak commented Feb 24, 2020

@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: 23f428b

@cgewecke
Copy link
Collaborator

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

@cgewecke
Copy link
Collaborator

@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?

@cgewecke cgewecke added 1.x 1.0 related issues Needs Clarification Requires additional input labels Feb 24, 2020
@teawaterwire
Copy link

teawaterwire commented Feb 25, 2020

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
Copy link
Contributor Author

VladoDemcak commented Feb 25, 2020

@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

  1. 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
  2. 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
Copy link
Contributor Author

I observe this problem on:

  • quorum-v2.4.0
  • TESTNET Kovan
  • Ganache on Mac v2.1.2

@cgewecke
Copy link
Collaborator

@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
Copy link
Contributor Author

VladoDemcak commented Feb 25, 2020

@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
Copy link
Collaborator

@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:

  • npm run test:e2e:ganache will execute the existing events tests on a ganache-cli instance.
  • there are sample ABIs and bytecodes available to experiment with in the test/sources folder

@VladoDemcak
Copy link
Contributor Author

VladoDemcak commented Feb 25, 2020

@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
Copy link
Contributor Author

VladoDemcak commented Feb 26, 2020

@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
Copy link
Contributor Author

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
Copy link
Collaborator

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
Copy link
Contributor Author

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

@cgewecke
Copy link
Collaborator

cgewecke commented Feb 28, 2020

@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
Copy link
Contributor Author

VladoDemcak commented Feb 28, 2020

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
Copy link
Collaborator

@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
Copy link
Collaborator

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

@VladoDemcak
Copy link
Contributor Author

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

@holgerd77
Copy link
Collaborator

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

@holgerd77 holgerd77 reopened this Feb 28, 2020
@VladoDemcak
Copy link
Contributor Author

VladoDemcak commented Feb 28, 2020

@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
Copy link
Collaborator

@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.

@cgewecke cgewecke added Bug Addressing a bug and removed Needs Clarification Requires additional input labels Feb 28, 2020
@VladoDemcak
Copy link
Contributor Author

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.x 1.0 related issues Bug Addressing a bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants