chainlink icon indicating copy to clipboard operation
chainlink copied to clipboard

[NODE] base64decode task failing to decode abi.encodePacked(_address) from decode_cbor task

Open vnavascues opened this issue 2 years ago • 1 comments

Description

Hi, I'm currently testing the new base64decode task and it errors with:

failed to decode base64 string: illegal base64 data at input byte 0

input: $(decode_cbor.address)

I'd like to know whether I'm misusing this new task or it is a bug. Thanks

Basic Information

The Solidity contract sends the address base64 encoded (as a map), via:

    function request(
        bytes32 _specId,
        uint256 _payment,
        address _address
    ) external {
        Chainlink.Request memory req = buildOperatorRequest(_specId, this.fulfill.selector);

        req.addBytes("address", abi.encodePacked(_address));

        sendOperatorRequest(req, _payment);
    }

The base64decode task input is vEyg7adkeoq3wgYcLhGKGKk28T0= (although after certain node version it is not visible anymore in the JSON taskRun):

"taskRuns": [
    {
      "__typename": "TaskRun",
      "id": "ff223dc5-ab89-4566-b94d-40e22977986d",
      "createdAt": "2022-08-10T12:29:39.568966Z",
      "dotID": "decode_log",
      "error": null,
      "finishedAt": "2022-08-10T12:29:39.569245Z",
      "output": "{"callbackAddr":"0x8F496457cA77f2Fa7c68A6F1B2127F87c0EE453d","callbackFunctionId":"0x5508ff94","cancelExpiration":1660134868,"data":"0x676164647265737354bc4ca0eda7647a8ab7c2061c2e118a18a936f13d","dataVersion":2,"payment":100000000000000000,"requestId":"0x21b4aea459a1148203fdd41f23bb3fcd318b8c19bbb042761f6d634fd809ac46","requester":"0x8F496457cA77f2Fa7c68A6F1B2127F87c0EE453d","specId":"0x3437366462353666313238623466613862306430313538393430393662666536"}",
      "type": "ethabidecodelog"
    },
    {
      "__typename": "TaskRun",
      "id": "e744f249-c385-456f-a8ea-8fc8e6a979fa",
      "createdAt": "2022-08-10T12:29:39.569362Z",
      "dotID": "decode_cbor",
      "error": null,
      "finishedAt": "2022-08-10T12:29:39.569445Z",
      "output": "{"address":"0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"}",
      "type": "cborparse"
    },
    {
      "__typename": "TaskRun",
      "id": "f48a0900-9b97-4e12-9a80-9e6b65dc43b6",
      "createdAt": "2022-08-10T12:29:39.569492Z",
      "dotID": "base64_decoder",
      "error": "failed to decode base64 string: illegal base64 data at input byte 0",
      "finishedAt": "2022-08-10T12:29:39.569621Z",
      "output": "null",
      "type": "base64decode"
    }
  ]

This is how I'd decode it using node.js (for instance in a base64decode external adapter):

b64addr = "vEyg7adkeoq3wgYcLhGKGKk28T0="
`0x${Buffer.from(b64addr, "base64").toString('hex')}` // '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d'
  • Network: Kovan
  • Node version: v1.7.0

Environment Variables N/A

Steps to Reproduce

Base64DecodeConsumer contract

Basic TOML spec:

type = "directrequest"
schemaVersion = 1
name = "Test base64decode task"
externalJobID = "476db56f-128b-4fa8-b0d0-15894096bfe6"
maxTaskDuration = "0s"
contractAddress = "0x878541888a928a31F9EAb4cB61DfD4e381EC2f00"
minContractPaymentLinkJuels = "10000000000000000"
observationSource = """
    decode_log          [type="ethabidecodelog"
                         abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)"
                         data="$(jobRun.logData)"
                         topics="$(jobRun.logTopics)"]

    decode_cbor         [type="cborparse" data="$(decode_log.data)"]

    base64_decoder      [type="base64decode" input="$(decode_cbor.address)"]

    decode_log -> decode_cbor -> base64_decoder
"""

JSON jobrun node 1.7.0

This picture shows how the base64 string was shown on old node versions: image

Additional Information Related issues: https://github.com/smartcontractkit/chainlink/issues/6555 https://github.com/smartcontractkit/documentation/issues/255

vnavascues avatar Aug 10 '22 12:08 vnavascues

thanks @vnavascues - will take a look.

zeuslawyer avatar Aug 11 '22 00:08 zeuslawyer

@vnavascues , thanks to @aelmanaa , I am able to offer you some suggestions. If it wasn't for his excellent help and careful explanations to me I would have struggled with this too!

  1. req.addBytes("address", abi.encodePacked(_address)) wont do the trick. It returns bytes (not a base64 encoded string). Your code is still sending bytes, though without the padding. Instead, .encodePacked() returns ABI ETH spec 32 bytes. Bytes (binary) is different from base64 which is technically a binary-to-string encoding and therefore different from ABI (bytes) encoding.

  2. Furthermore, .encodePacked is unpadded. Instead we need to use encode() [footnote A] which pads with extra zeroes to achieve 32 bytes, and is what works on the CL Node's ETH ABI Decode Task. Turns out that anything that is not 32 bytes is problematic for the CBOR decoding [footnote B].

So if you use abi.encode() and change your TOML to use an ethabidecode task type instead of a base64decode task type, it will work. The docs for addBytes method on Chainlink.Request also recommends that we can pass an address using abi.encode [footnote C]

It would look like this:

function request( ) external {
        Chainlink.Request memory req = buildOperatorRequest('###JOB_SPEC_ID_HERE', this.fulfill.selector);
      
        req.addBytes("address", abi.encode(address(this)));
        sendOperatorRequest(req, (1 * LINK_DIVISIBILITY) / 10);
    }

The relevant Decode Task for the TOML job looks like this when you abi.encode() the address.

abidecode [type="ethabidecode"
        abi="address address"
        data="$(decode_cbor.address)"] 
  1. to play with the base 64 encode task, please use this gist that @aelmanaa has produced. He has used the Open Zeppelin library to convert the address to a base64 String and send that in the request. Note the use of req.add instead of .addBytes.
    add is used for strings, which is what the base 64 encoding produces.

The relevant part of the job's TOML with the tasks looks like this:

observationSource = """
    decode_log          [type="ethabidecodelog"
                         abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)"
                         data="$(jobRun.logData)"
                         topics="$(jobRun.logTopics)"]

    decode_cbor         [type="cborparse" data="$(decode_log.data)"]

    base64_decoder      [type="base64decode" input="$(decode_cbor.address)"]

    decode_log -> decode_cbor -> base64_decoder

Once again, a big thanks to @aelmanaa who helped a lot in understanding this.

[A] As per @aelmanaa's comment in Issue #6555, [B] As per @aelmanaa's comment in smartcontractkit/documentation#255 [C] https://docs.chain.link/docs/chainlink-framework/#addbytes

zeuslawyer avatar Aug 12 '22 08:08 zeuslawyer

@zeuslawyer @aelmanaa thank you so much for this thorough explanation and the time spent on it, amazing.

  • Wrt req.addBytes(): I thought that it was sending the data as a base64 encoded string.
  • Wrt ethabidecode: it is the only classic tasks I've barely used. In fact my first time was trying to move away from passing an address as string (this is where I found out the aelmanaa issues). Somehow, I misused ethabidecode (probably due to abi.encodePacked) and eventually found another way.

I've just successfully tested sending address and address[] via addBytes() and decoding them with ethabidecode. And I get now the usecase of base64decode and how send the data from the consumer contract.

Thank you!

vnavascues avatar Aug 12 '22 11:08 vnavascues

no problems @vnavascues - glad this helped.

so did you encode address an address[] using encode() before sending them? i.e. it worked when you encoded them to bytes32 ?

zeuslawyer avatar Aug 12 '22 11:08 zeuslawyer

no problems @vnavascues - glad this helped.

so did you encode address an address[] using encode() before sending them? i.e. it worked when you encoded them to bytes32 ?

Yep, with abi.encode() it worked:

    function request(
        bytes32 _specId,
        uint256 _payment,
        address _address
    ) external {
        Chainlink.Request memory req = buildOperatorRequest(_specId, this.fulfill.selector);

        req.addBytes("address", abi.encode(_address));

        sendOperatorRequest(req, _payment);
    }

    function requestArray(
        bytes32 _specId,
        uint256 _payment,
        address[] memory _addresses
    ) external {
        Chainlink.Request memory req = buildOperatorRequest(_specId, this.fulfill.selector);

        req.addBytes("addresses", abi.encode(_addresses));

        sendOperatorRequest(req, _payment);
    }

And the TOML you shared:

abidecode [type="ethabidecode" abi="address address" data="$(decode_cbor.address)"] 
abidecode [type="ethabidecode" abi="address[] addresses" data="$(decode_cbor.addresses)"] 

Feel free to close the issue

vnavascues avatar Aug 12 '22 12:08 vnavascues

Great, thanks for confirming.

zeuslawyer avatar Aug 12 '22 12:08 zeuslawyer