|
| 1 | +# How to perform HTTP requests |
| 2 | + |
| 3 | +This example application demonstrates how to perform HTTP requests from the service and from the |
| 4 | +contract, in a few different ways: |
| 5 | + |
| 6 | +- From the service while handling a mutation. |
| 7 | +- From the contract directly. |
| 8 | +- From the service when it is being used as an oracle by the contract. |
| 9 | + |
| 10 | +## HTTP requests from the service |
| 11 | + |
| 12 | +The service is executed either on the client when requested by the user or on validators when the |
| 13 | +service is queried as an oracle by the contract. In this first usage scenario, the HTTP request is |
| 14 | +executed only in the client. |
| 15 | + |
| 16 | +The HTTP response can then be used by the service to either prepare a query response to the caller |
| 17 | +or to prepare operations to be executed by the contract in a block proposal. |
| 18 | + |
| 19 | +## HTTP requests from the contract |
| 20 | + |
| 21 | +The contract can perform HTTP requests as well, but the responses must always be the same. The |
| 22 | +requests are executed on the client and on all the validators. That means that the client and each |
| 23 | +validator perform the HTTP request independently. The responses must all match (or at least match |
| 24 | +in a quorum of validators) for the block the be confirmed. |
| 25 | + |
| 26 | +If the response varies per request (as a simple example, due to the presence of a "Date" timestamp |
| 27 | +header in the response), the block proposal may end up being rejected by the validators. If there's |
| 28 | +a risk of that happening, the contract should instead call the service as an oracle, and let the |
| 29 | +service perform the HTTP request and return only the deterministic parts of the response. |
| 30 | + |
| 31 | +## HTTP requests using the service as an oracle |
| 32 | + |
| 33 | +The contract may call the service as an oracle. That means that that contracts sends a query to the |
| 34 | +service and waits for its response. The execution of the contract is metered by executed |
| 35 | +instruction, while the service executing as an oracle is metered by a coarse-grained timer. That |
| 36 | +allows the service to execute non-deterministically, and as long as it always returns a |
| 37 | +deterministic response back to the contract, the validators will agree on its execution and reach |
| 38 | +consensus. |
| 39 | + |
| 40 | +In this scenario, the contract requests the service to perform the HTTP request. The HTTP request |
| 41 | +is also executed in each validator. |
| 42 | + |
| 43 | +## Recommendation |
| 44 | + |
| 45 | +It is recommended to minimize the number of HTTP requests performed in total, in order to reduce |
| 46 | +costs. Whenever possible, it's best to perform the request in the client using the service, and |
| 47 | +forward only the HTTP response to the contract. The contract should then verify that the response |
| 48 | +can be trusted. |
| 49 | + |
| 50 | +If there's no way to verify an off-chain HTTP response in the contract, then the request should be |
| 51 | +made in the contract. However, if there's a risk of receiving different HTTP responses among the |
| 52 | +validators, the contract should use the service as oracle to perform the HTTP request and return to |
| 53 | +the contract only the data that is deterministic. Using the service as an oracle is more expensive, |
| 54 | +so it should be avoided if possible. |
| 55 | + |
| 56 | +## Usage |
| 57 | + |
| 58 | +### Setting Up |
| 59 | + |
| 60 | +Before getting started, make sure that the binary tools `linera*` corresponding to |
| 61 | +your version of `linera-sdk` are in your PATH. |
| 62 | + |
| 63 | +For the test, a simple HTTP server will be executed in the background. |
| 64 | + |
| 65 | +```bash |
| 66 | +HTTP_PORT=9090 |
| 67 | +cd examples |
| 68 | +cargo run --bin test_http_server -- "$HTTP_PORT" & |
| 69 | +cd .. |
| 70 | +``` |
| 71 | + |
| 72 | +From the root of Linera repository, the environment can be configured to provide a `linera_spawn` |
| 73 | +helper function useful for scripting, as follows: |
| 74 | + |
| 75 | +```bash |
| 76 | +export PATH="$PWD/target/debug:$PATH" |
| 77 | +source /dev/stdin <<<"$(linera net helper 2>/dev/null)" |
| 78 | +``` |
| 79 | + |
| 80 | +To start the local Linera network: |
| 81 | + |
| 82 | +```bash |
| 83 | +linera_spawn linera net up --with-faucet --faucet-port 8081 |
| 84 | + |
| 85 | +# Remember the URL of the faucet. |
| 86 | +FAUCET_URL=http://localhost:8081 |
| 87 | +``` |
| 88 | + |
| 89 | +We then create a wallet and obtain a chain to use with the application. |
| 90 | + |
| 91 | +```bash |
| 92 | +export LINERA_WALLET="$LINERA_TMP_DIR/wallet.json" |
| 93 | +export LINERA_STORAGE="rocksdb:$LINERA_TMP_DIR/client.db" |
| 94 | + |
| 95 | +linera wallet init --faucet $FAUCET_URL |
| 96 | + |
| 97 | +INFO=($(linera wallet request-chain --faucet $FAUCET_URL)) |
| 98 | +CHAIN="${INFO[0]}" |
| 99 | +``` |
| 100 | + |
| 101 | +Now, compile the application WebAssembly binaries, publish and create an application instance. |
| 102 | + |
| 103 | +```bash |
| 104 | +(cd examples/how-to/perform-http-requests && cargo build --release --target wasm32-unknown-unknown) |
| 105 | + |
| 106 | +APPLICATION_ID=$(linera publish-and-create \ |
| 107 | + examples/target/wasm32-unknown-unknown/release/how_to_perform_http_requests_{contract,service}.wasm \ |
| 108 | + --json-parameters "\"http://localhost:$HTTP_PORT\"") |
| 109 | +``` |
| 110 | + |
| 111 | +The `APPLICATION_ID` is saved so that it can be used in the GraphQL URL later. But first the |
| 112 | +service that handles the GraphQL requests must be started. |
| 113 | + |
| 114 | +```bash |
| 115 | +PORT=8080 |
| 116 | +linera service --port $PORT & |
| 117 | +``` |
| 118 | + |
| 119 | +#### Using GraphiQL |
| 120 | + |
| 121 | +Type each of these in the GraphiQL interface and substitute the env variables with their actual |
| 122 | +values that we've defined above. |
| 123 | + |
| 124 | +- Navigate to the URL you get by running `echo "http://localhost:8080/chains/$CHAIN/applications/$APPLICATION_ID"`. |
| 125 | +- To query the service to perform an HTTP query locally: |
| 126 | +```gql,uri=http://localhost:8080/chains/$CHAIN/applications/$APPLICATION_ID |
| 127 | +query { performHttpRequest } |
| 128 | +``` |
| 129 | +- To make the service perform an HTTP query locally and use the response to propose a block: |
| 130 | +```gql,uri=http://localhost:8080/chains/$CHAIN/applications/$APPLICATION_ID |
| 131 | +mutation { performHttpRequest } |
| 132 | +``` |
| 133 | +- To make the contract perform an HTTP request: |
| 134 | +```gql,uri=http://localhost:8080/chains/$CHAIN/applications/$APPLICATION_ID |
| 135 | +mutation { performHttpRequestInContract } |
| 136 | +``` |
| 137 | +- To make the contract use the service as an oracle to perform an HTTP request: |
| 138 | +```gql,uri=http://localhost:8080/chains/$CHAIN/applications/$APPLICATION_ID |
| 139 | +mutation { performHttpRequestAsOracle } |
| 140 | +``` |
0 commit comments