Welcome to Town Crier’s documentation!¶
How Town Crier Works¶
The Big Picture¶
Town Crier (TC) connects the authenticated data from HTTPS websites to smart contracts. TC works in a request-response fashion—the client (your fantastic smart contract that needs data) submit queries to TC, from which responses are furnished.
A Little Bit More Details¶
Behind the scenes, TC has two components that work together to serve queries from client contracts:
- TC Contract
- The frontend. A smart contract deployed on the blockchain that is responsible to interface with client contracts.
- TC Server
- The backend. A SGX-protected process that actually handles the queries picked up by the fronend. When the TC contract receives a query from a client contract, the TC server fetches the requested data from the website and relays it back to the requesting contract.
Security of TC¶
Query processing happens inside an SGX-protected environment known as an enclave. The requested data is fetched via a TLS connection to the target website that terminates inside the enclave. SGX protections prevent even the operating system of the server from peeking into the enclave or modifying its behavior, while use of TLS prevents tampering or eavesdropping on communications on the network.
Town Crier can optionally ingest an encrypted query, allowing it to handle secret query data. For example, a query could include a password used to log into a server or secret trading data. TC’s operation in an SGX enclave ensures that the password or trading data is concealed from the TC operator (and everyone else).
Get Started: Write Your First TC-powered Contract¶
The TC contract has a very simple API for your contract to call. Below we present the generic API, and then we’ll go through an example (Play with TC on Rinkeby) with detailed explanation.
Submit queries via request
¶
An application contract sends queries to TC by calling the request
function.
request(uint8 requestType, address callbackAddr,
bytes4 callbackFID, uint256 timestamp,
bytes32[] requestData) public payable returns(int256);
- Parameters:
requestType
: indicates the query type (see below).callbackAddr
: the address of the recipient contract (see below).callbackFID
: the callback function selector (see below).timestamp
: reserved. Unused for now.requestData
: data specifying query parameters. The format depends on the query type.
Warning
Calling contracts must prepay the gas cost incurred by the Town Crier server in delivering a response to the application contract properly. The calling contract is responsible to set msg.value
properly.
Return: The function returns an int256
value denoted as requestId
.
- If
requestId > 0
, then the request is successfully submitted. The calling contract can userequestId
to check the response or status.- If
requestId = -2^250
, then the request fails because the requester didn’t send enough fee to the TC Contract.- If
requestId = 0
, then the TC service is suspended due to some internal reason. No more requests or cancellations can be made but previous requests will still be responded to by TC.- If
requestId < 0 && requestId != -2^250
, then the TC Contract is upgraded and requests should be sent to the new address-requestId
(negative ofrequestId
).
Canceling requests via cancel
¶
cancel(uint64 requestId) public returns(bool);
Unprocessed requests can be canceled by calling function cancel(requestId)
.
The fee paid by the requester is refunded (minus processing costs, denoted as cancellation fee).
For more details about how Town Crier contract works, you can look at the source code of the contract TownCrier.sol.
Receiving responses from TC¶
To receive a response from TC, the requester need to specify the recipient contract as well as the recipient function.
Warning
Very importantly, TC requires that the recipient function to have the following signature.
function FUNCTION_NAME(uint64 requestId, uint64 error, bytes32 respData) public;
This is the function that will be called by the TC Contract to deliver the response from TC server.
To do so, you should set the callbackFID
parameter to bytes4(sha3("FUNCTION_NAME(uint64,uint64,bytes32)"))
, namely the selector of your FUNCTION_NAME
function.
- Parameters:
error
andrespData
. - If
error = 0
, the request has been successfully processed and the calling contract can then safely userespData
. The fee paid is consumed by TC. - If
error = 1
, the provided request is invalid or cannot be found on the website. In this case, similarly, the fee is consumed by TC. - If
error > 1
, then an error has occured in the Town Crier server. In this case, the fee is fully refunded but the transaction fee (for making this call).
- If
Play with TC on Rinkeby¶
Note
A version of Town Crier smart contract has been deployed on the Rinkeby testnet.
To show how to use Town Crier, we present a skeleton Application
Contract that does nothing other than sending queries, logging responses and cancelling queries.
The Application contract has also been deployed on Rinkeby (https://rinkeby.etherscan.io/address/0x20e63d9683a75ef73e6174298354f8b016878de3).
The source code of the application contract can be found here. Now we go through the contract code line by line.
Preliminaries¶
First, you need to annotate your contract with the version pragma:
pragma solidity ^0.4.9;
Second, you need to include in your contract the function declaration of the TownCrier
Contract so that the application contract can call those functions with the address of the TownCrier
Contract.
contract TownCrier {
function request(uint8 requestType,
address callbackAddr,
bytes4 callbackFID,
uint timestamp,
bytes32[] requestData) public payable returns (uint64);
function cancel(uint64 requestId) public returns (int);
}
Note
You do not need to include response()
here because an appilcation contract should not make a function call to it but wait for being called by it.
Outline of Application.sol
¶
Let’s look at the layout of the Application
Contract:
contract Application {
event Request(int64 requestId, address requester, uint dataLength, bytes32[] data);
event Response(int64 requestId, address requester, uint64 error, uint data);
event Cancel(uint64 requestId, address requester, bool success);
bytes4 constant TC_CALLBACK_FID = bytes4(sha3("response(uint64,uint64,bytes32)"));
address[2**64] requesters;
uint[2**64] fee;
function() public payable;
function Application(TownCrier tcCont) public;
function request(uint8 requestType, bytes32[] requestData) public payable;
function response(uint64 requestId, uint64 error, bytes32 respData) public;
function cancel(uint64 requestId) public;
}
- The events
Request()
,Response
andCancel()
keeps logs of therequestId
assigned to a query, the response from TC and the result of a cancellation respectively for a user to fetch from the blockchain. - The constant
TC_CALLBACK_FID
is the first 4 bytes of the hash of the functionresponse()
that theTownCrier
Contract calls when relaying the response from TC. The name of the callback function can differ but the three parameters should be exactly the same as in this example. - The address array
requesters
stores the addresses of the requesters. - The uint array
fee
stores the amounts of wei requesters pay for their queries.
As you can see above, the Application
Contract consists of a set of five basic functions:
Default Function¶
function() public payable;
This fallback function must be payable so that TC can provide a refund under certain conditions. The fallback function should not cost more than 2300 gas, otherwise it will run out of gas when TC refunds ether to it. In our contract, it simply does nothing.
function() public payable {}
Constructor¶
function Application(TownCrier tc) public;
This is the constructor which registers the address of the TC Contract and the owner of this contract during creation so that it can call the request()
and cancel()
functions in the TC contract.
TownCrier public TC_CONTRACT;
address owner;
function Application(TownCrier tcCont) public {
TC_CONTRACT = tcCont;
owner = msg.sender;
}
Submitting Requests¶
function request(uint8 requestType, bytes32[] requestData) public payable;
A user calls this function to send a request to the Application
Contract. This function forwards the query to the request()
of the TC Contract by
requestId = TC_CONTRACT.request.value(msg.value)(requestType, TC_CALLBACK_ADD, TC_CALLBACK_FID, timestamp, requestData);
msg.value
is the fee the user pays for this request. TC_CALLBACK_ADD
is the address of the callback contract. or this
for the current contract. TC_CALLBACK_FID
is the first 4 bytes of the hash of the callback function signature, as defined above.
uint constant MIN_GAS = 30000 + 20000;
uint constant GAS_PRICE = 5 * 10 ** 10;
uint constant TC_FEE = MIN_GAS * GAS_PRICE;
function request(uint8 requestType, bytes32[] requestData) public payable {
if (msg.value < TC_FEE) {
// If the user doesn't pay enough fee for a request,
// we should discard the request and return the ether.
if (!msg.sender.send(msg.value)) throw;
return;
}
int requestId = TC_CONTRACT.request.value(msg.value)(requestType, this, TC_CALLBACK_FID, 0, requestData);
if (requestId == 0) {
// If the TC Contract returns 0 indicating the request fails
// we should discard the request and return the ether.
if (!msg.sender.send(msg.value)) throw;
return;
}
// If the request succeeds,
// we should record the requester and how much fee he pays.
requesters[uint64(requestId)] = msg.sender;
fee[uint64(requestId)] = msg.value;
Request(int64(requestId), msg.sender, requestData.length, requestData);
}
Warning
Developers need to send enough fee.
TC requires at least 3e4 gas for all the operations (besides calling the callback function). The gas price is set to 5e10 wei. So the caller should pay at least (3e4 + callback_gas) * 5e10 wei. Otherwise the request
call will fail (and the TC Contract will return 0 as requestId
). Developers should handle this failure.
For our Application.sol
, the callback function (response
) costs about 2e4 gas, so the caller should pay no less than (3e4 + 2e4) * 5e10 = 2.5e15 wei (denoted as TC_FEE
).
Note
TC server sets the gas limit as 3e6 when sending the response to the TC Contract. If a requester paid more gas than that, the excess ether will not be used for the callback function. It will go directly to the SGX wallet. This is a way to offer a tip for the Town Crier service.
Receiving Responses¶
function response(uint64 requestId, uint64 error, bytes32 respData) public;
This is the function to be called by the TC Contract to deliver the response from TC server. The selector for this function is passed to the request call. See Submitting Requests.
function response(uint64 requestId, uint64 error, bytes32 respData) public {
// If the response is not sent from the TC Contract,
// we should discard the response.
if (msg.sender != address(TC_CONTRACT)) return;
address requester = requesters[requestId];
// Set the request state as responded.
requesters[requestId] = 0;
if (error < 2) {
// If either TC responded with no error or the request is invalid by the requester's fault,
// public the response on the blockchain by event Response().
Response(int64(requestId), requester, error, uint(respData));
} else {
// If error exists by TC's fault,
// fully refund the requester.
requester.send(fee[requestId]);
Response(int64(requestId), msg.sender, error, 0);
}
}
Warning
Since the gas limit for sending a response back to the TC Contract is set as 3e6 by the Town Crier server, as mentioned above, the callback function should not consume more gas than that. Otherwise the callback function will run out of gas and fail. The TC service does not take responsibility for such failures, and treats queries that fail in this way as successfully responded to.
To estimate how much gas the callback function costs, you can use web3.eth.estimateGas.
Cancellation¶
function cancel(uint64 requestId) public;
This function calls the cancel()
function of the TC Contract, to cancel a unprocessed request.
uint constant CANCELLATION_FEE = 25000 * GAS_PRICE;
function cancel(uint64 requestId) public {
// If the cancellation request is not sent by the requester himself,
// discard the cancellation request.
if (requestId == 0 || requesters[requestId] != msg.sender) return;
bool tcCancel = TC_CONTRACT.cancel(requestId);
if (tcCancel) {
// If the cancellation succeeds,
// set the request state as cancelled and partially refund the requester.
requesters[requestId] = 0;
if (!msg.sender.send(fee[requestId] - CANCELLATION_FEE)) throw;
Cancel(requestId, msg.sender, true);
}
}
TC charges 2.5e4 * 5e10 = 1.25e15 wei, denoted as CANCELLATION_FEE
here, for cancellation.
In this function a user is partially refunded fee - CANCELLATION_FEE
. A developer must carefully set a cancelled flag for the request before refunding the requester in order to prevent reentrancy attacks.
Send queries to Application.sol¶
You can play with the Application.sol
deployed on Rinkeby testnet, at 0xdE34AfC49b8A15bEb76A6E942bD687143C1574B6.
Assuming we’re at the geth console loaded with the following script. You can find a script for this purpose here.
function createApp(tc) {
unlockAccounts();
var tradeContract = App.new(
tc, {
from: tcDevWallet,
data: "0x" + compiledContract.contracts["Application"].bin,
gas: gasCnt
},
function (e, c) {
if (!e) {
if (c.address) {
console.log('Application created at: ' + c.address)
}
} else {
console.log('Failed to create Application contract: ' + e)
}
});
return tradeContract;
}
function request(contract, type, requestData) {
unlockAccounts();
contract.request.sendTransaction(type, requestData, {
from: tcDevWallet,
value: 3e15,
gas: gasCnt
});
return "Request sent!";
}
function watch_events(contract) {
var his = contract.allEvents({fromBlock: 0, toBlock: 'latest'});
var events;
his.get(function (error, result) {
if (!error) {
console.log(result.length);
for (var i = 0; i < result.length; ++i) {
console.log(i + " : " + result[i].event);
}
events = result;
} else {
console.log("error");
events = "error";
}
});
return events;
}
Let’s try to trigger Application.sol to query for bitcoin price (from coinmarketcap.com) and Bitcoin Fee.
First, create an instance of Application.sol.
> var App = web3.eth.contract(JSON.parse("[{"constant":false,"inputs":[{"name":"requestType","type":"uint8"},{"name":"requestData","type":"bytes32[]"}],"name":"request","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"requestId","type":"uint64"}],"name":"cancel","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"TC_CONTRACT","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"requestId","type":"uint64"},{"name":"error","type":"uint64"},{"name":"respData","type":"bytes32"}],"name":"response","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"tcCont","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"requestId","type":"int64"},{"indexed":false,"name":"requester","type":"address"},{"indexed":false,"name":"dataLength","type":"uint256"},{"indexed":false,"name":"data","type":"bytes32[]"}],"name":"Request","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"requestId","type":"int64"},{"indexed":false,"name":"requester","type":"address"},{"indexed":false,"name":"error","type":"uint64"},{"indexed":false,"name":"data","type":"uint256"}],"name":"Response","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"requestId","type":"uint64"},{"indexed":false,"name":"requester","type":"address"},{"indexed":false,"name":"success","type":"bool"}],"name":"Cancel","type":"event"}]"));
> app = App.at("0xdE34AfC49b8A15bEb76A6E942bD687143C1574B6");
Now, send a few requests!
> request(app, 2, []); // get current bitcoin transaction fee
> request(app, 5, ['bitcoin']);"; // get current bitcoin price
To see the responses (and the requests), examine the log:
> watch_events(app);
You’ll see something like this for bitcoin transaction fee query:
9 : Request
{
"args":{
"data":[
],
"dataLength":"0",
"requestId":"5",
"requester":"0x8f108aab17e3b90f6855a73349511f5944b7e146"
},
"blockNumber":2182246,
"transactionHash":"0x027f8b992b65b58f1aa2191e6ae55d1c074cdaa475a71823d1c879ddc8cbae79",
}
10 : Response
{
"args":{
"data":"100", // fastestFee=100 from https://bitcoinfees.earn.com/api/v1/fees/recommended
"error":"0",
"requestId":"5",
"requester":"0x8f108aab17e3b90f6855a73349511f5944b7e146"
},
"blockNumber":2182248,
"transactionHash":"0x630d8f7fae392c2ff6a0115956e72cad5fae4e008f3ef5e543d02c112a7d5cf5",
}
For bitcoin price query:
11 : Request
{
"args":{
"data":[
// ascii of 'bitcoin', to get the current bitcoin price
"0x626974636f696e00000000000000000000000000000000000000000000000000"
],
"dataLength":"1",
"requestId":"6",
"requester":"0x8f108aab17e3b90f6855a73349511f5944b7e146"
},
"blockNumber":2182269,
"transactionHash":"0x673a1db9c675646c6319959f879bd8a3f711393667e6343c2125e707a70e8616",
}
12 : Response
{
"args":{
"data":"9204", // bitcoin price is 9204 USD at Fri Apr 27 00:44:58 EDT 2018
"error":"0",
"requestId":"6",
"requester":"0x8f108aab17e3b90f6855a73349511f5944b7e146"
},
"blockNumber":2182271,
"transactionHash":"0x1464d26cbab1238ce8ac4ac48cd2019425be59c451099d2437056ac6c253bf40",
}
How to use various docker images¶
The preferred way to launch TC is via docker services. Of course, you’ll need docker and docker-compose properly installed.
Setup¶
First, get the docker service files from
git clone https://github.com/bl4ck5un/Town-Crier-docker-sevices
cd rinkeby
The current version of TC uses Infura as a Web3 provider to access Rinkeby testnet.
(Support for other networks as well as private nets are being added.)
To use Infura, set the following environment variables (or put them in a .env
file):
Warning
Keep WEB3_INFURA_PROJECT_ID
and WEB3_INFURA_API_SECRET
secrets.
Launch TC¶
Thanks to docker-compose, TC—the backend as well as the relay—can be launch together in one command: docker-compose up
.
To run it in the background, use docker-compose up -d
.
Other advanced uses of docker-compose
can be found in their documentation.
There is a convenience Makefile that wraps around common docker-compose commands.
For example, you can type make up
instead of docker-compose up
.
Technical details of the TownCrier.sol
¶
The TownCrier
contract provides a uniform interface for queries from and replies to an application contract, which we also refer as a “Requester”.
This interface consists of the following three functions.
request(uint8 requestType, address callbackAddr, \
bytes4 callbackFID, uint256 timestamp, \
bytes32[] requestData) public payable returns(uint64);
An application contract sends queries to TC by calling function request()
, and it needs to send the following parameters.
requestType
: indicates the query type. You can find the query types and respective formats that Town Crier currently supports on the Dev page.callbackAddr
: the address of the application contract to which the response from Town Crier is forwarded.callbackFID
: specifies the callback function in the application contract to receive the response from TC.timestamp
: currently unused parameter. This parameter will be used in a future feature. Currently TC only responds to requests immediately. Eventually TC will support requests with a future query time pre-specified bytimestamp
. At present, developers can ignore this parameter and just set it to 0.requestData
: data specifying query parameters. The format depends on the query type.
When the request
function is called, a request is logged by event RequestInfo()
.
The function returns a requestId
that is uniquely assigned to this request.
The application contract can use the requestId
to check the response or status of a request in its logs.
The Town Crier server watches events and processes a request once logged by RequestInfo()
.
Requesters must prepay the gas cost incurred by the Town Crier server in relaying a response to the application contract.
msg.value
is the amount of wei a requester pays and is recorded as Request.fee
.
deliver(uint64 requestId, bytes32 paramsHash, uing64 error, bytes32 respData) public;
After fetching data and generating the response for the request with requestId
, TC sends a transaction calling function deliver()
.
deliver()
verifies that the function call is made by SGX and that the hash of query parameters is correct.
Then it calls the callback function of the application contract to transmit the response.
The response includes error
and respData
.
If error = 0
, the application contract request has been successfully processed and the application contract can then safely use respData
.
The fee paid by the application contract for the request is consumed by TC.
If error = 1
, the application contract request is invalid or cannot be found on the website.
In this case, similarly, the fee is consumed by TC.
If error > 1
, then an error has occured in the Town Crier server.
In this case, the fee is fully refunded but the transaction cost for requesting by the application contract won’t be compensated.
cancel(uint64 requestId) public returns(bool);
A requester can cancel a request whose response has not yet been issued by calling function cancel()
.
requestId
is required to specify the query.
The fee paid by the Appliciation Contract is then refunded (minus processing costs, denoted as cancellation fee).
For more details, you can look at the source code of the contract [TownCrier.sol].