Turing BOBA Faucet
Turing Example - CAPTCHA-based token faucet
Boba Faucet is a system for distributing Rinkeby ETH and Rinkeby BOBA. It's implemented using Turing hybrid compute. Before claiming tokens, users answer a CAPTCHA. Their answer is hashed and compared off-chain to the correct answer via Turing. Once their answer is verified, the smart contract releases the funds.

Directory Structure

  • boba_community/turing-captcha-faucet/packages: Contains all the typescript packages and contracts
    • contracts: Smart contracts implementing the Boba Faucet
    • gateway: The Boba Web faucet frontend
    • deployment: Boba faucet Rinkeby contract addresses
    • api: Boba faucet backend API

Specification

The token-claiming process takes place in five steps:

1. User obtains the CAPTCHA image and the image UUID

The API GET request is sent to https://api-turing.boba.network/get.catcha on the frontend. The returned payload is
1
{
2
"UUID": "BYTES32",
3
"imageBase64": "CAPTCHA IMAGE"
4
}
Copied!
The UUID and hashed CAPTCHA answer are stored in AWS Redis.

2. User sends a transaction to the Boba Faucet contract with the UUID and CAPTCHA answer

1
const BobaFaucet = new ethers.Contract(
2
BOBA_FAUCET_CONTRACE_ADDRESS,
3
BobaFaucetJson.abi,
4
this.provider.getSigner()
5
)
6
const tx = await BobaFaucet.getBobaFaucet(uuid, answer)
7
await tx.wait()
Copied!
The answer is hashed in the Boba Faucet contract first before sending it to backend API.
1
bytes32 hashedKey = keccak256(abi.encodePacked(_key));
2
bytes memory encRequest = abi.encodePacked(_uuid, hashedKey);
3
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
Copied!

3. Geth sends a request to the backend and retrieves the result

The POST request with the hashed answer is sent to https://api-turing.boba.network/verify.captcha. It decodes the input and verifies the UUID with the hashed answer.
1
paramsHexString = body['params'][0]
2
# Select uuid and answer
3
# paramsHexString[0: 66] is the length of input data
4
uuid = '0x' + paramsHexString[66: 130]
5
answer = '0x' + paramsHexString[130:]
6
# Get answer from Redis
7
keyInRedis = db.get(uuid)
8
# Return the payload
9
return returnPayload(keyInRedis.decode('utf-8') == key)
10
11
# Payload is built based on the result
12
def returnPayload(result):
13
# We return 0 or 1 using uint256
14
payload = '0x' + '{0:0{1}x}'.format(int(32), 64)
15
if result:
16
# Add UINT256 1 if the result is correct
17
payload += '{0:0{1}x}'.format(int(1), 64)
18
else:
19
# Add UINT256 0 if the result is wrong
20
payload += '{0:0{1}x}'.format(int(0), 64)
21
22
returnPayload = {
23
'statusCode': 200,
24
'body': json.dumps({ 'result': payload })
25
}
26
27
return returnPayload
Copied!

4. Geth atomically revises the calldata

On the contract level, we decode the result from the Turing request and release the funds if the answer is correct.
1
2
3
// Decode the response from outside API
4
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
5
uint256 result = abi.decode(encResponse,(uint256));
6
// Release the funds if it is correct
7
require(result == 1, 'Captcha wrong');
8
IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);
Copied!

5. User obtains the funds if the answer is correct or sees an error message

Implementation

Step 1: Creating API endpoints

Two simple API endpoints are created.
get.captcha
1
image = ImageCaptcha(width=280, height=90)
2
3
# For image
4
uuid1 = uuid.uuid4()
5
imageStr = str(uuid1).split('-')[0]
6
imageStrBytes = Web3.solidityKeccak(['string'], [str(imageStr)]).hex()
7
8
# For key
9
uuid2 = uuid.uuid4()
10
keyBytes = Web3.solidityKeccak(['string'], [str(uuid2)]).hex()
11
12
# Use the first random string
13
imageData = image.generate(imageStr)
14
15
# Get base64
16
imageBase64 = base64.b64encode(imageData.getvalue())
17
18
# Read YML
19
with open("env.yml", 'r') as ymlfile:
20
config = yaml.load(ymlfile)
21
22
# Connect to Redis Elasticache
23
db = redis.StrictRedis(host=config.get('REDIS_URL'),
24
port=config.get('REDIS_PORT'))
25
26
# Store and set the expire time as 10 minutes
27
print('keyBytes: ', keyBytes, 'imageStrBytes: ', imageStrBytes, 'imageStr: ', imageStr)
28
db.set(keyBytes, imageStrBytes, ex=600)
29
30
payload = {'uuid': keyBytes, 'imageBase64': imageBase64.decode('utf-8') }
Copied!
verify.captcha
1
paramsHexString = body['params'][0]
2
3
uuid = '0x' + paramsHexString[66: 130]
4
key = '0x' + paramsHexString[130:]
5
6
# Read YML
7
with open("env.yml", 'r') as ymlfile:
8
config = yaml.load(ymlfile)
9
10
# Connect to Redis Elasticache
11
db = redis.StrictRedis(host=config.get('REDIS_URL'),
12
port=config.get('REDIS_PORT'))
13
14
# Store and set the expire time as 10 minutes
15
keyInRedis = db.get(uuid)
16
17
if keyInRedis:
18
print('keyInRedis: ', keyInRedis.decode('utf-8'), 'key: ', key)
19
return returnPayload(keyInRedis.decode('utf-8') == key)
20
else:
21
return returnPayload(False)
Copied!

Step 2: Creating the Boba Faucet Contract

BOBA Faucet Smart Contracts

Deployment

Create a .env file in the root directory of the contracts folder. Add environment-specific variables on new lines in the form of NAME=VALUE. Examples are given in the .env.example file. Just pick which net you want to work on and copy either the "Rinkeby" or the "Local" envs to your .env.
1
NETWORK=rinkeby
2
L1_NODE_WEB3_URL=https://rinkeby.infura.io/v3/9844f35ff4a84003a7025a65a9412002
3
L2_NODE_WEB3_URL=https://rinkeby.boba.network
4
ADDRESS_MANAGER_ADDRESS=0x93A96D6A5beb1F661cf052722A1424CDDA3e9418
5
DEPLOYER_PRIVATE_KEY=
Copied!
Build and deploy all the needed contracts:
1
$ yarn build
2
$ yarn deploy
Copied!
The smart contract imports the Turing Helper Contract so it can interact with outside API endpoints.
1
import './TuringHelper.sol';
2
3
contract BobaFaucet is Ownable {
4
// Turing
5
address public turingHelperAddress;
6
string public turingUrl;
7
TuringHelper public turing;
8
9
constructor(
10
address _turingHelperAddress,
11
string memory _turingUrl
12
) {
13
turingHelperAddress = _turingHelperAddress;
14
turing = TuringHelper(_turingHelperAddress);
15
turingUrl = _turingUrl;
16
}
17
18
function getBobaFaucet(
19
bytes32 _uuid,
20
string memory _key
21
) external {
22
require(BobaClaimRecords[msg.sender] + waitingPeriod < block.timestamp, 'Invalid request');
23
24
// The key is hashed
25
bytes32 hashedKey = keccak256(abi.encodePacked(_key));
26
uint256 result = _verifyKey(_uuid, hashedKey);
27
require(result == 1, 'Captcha wrong');
28
29
BobaClaimRecords[msg.sender] = block.timestamp;
30
IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);
31
32
emit GetBobaFaucet(_uuid, hashedKey, msg.sender, BobaFaucetAmount, block.timestamp);
33
}
34
35
// Call Turing contract to get the result from the outside API endpoint
36
function _verifyKey(bytes32 _uuid, bytes32 _key) private returns (uint256) {
37
bytes memory encRequest = abi.encodePacked(_uuid, _key);
38
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
39
40
uint256 result = abi.decode(encResponse,(uint256));
41
42
emit VerifyKey(turingUrl, _uuid, _key, result);
43
44
return result;
45
}
46
}
Copied!

Step 3: Funding Turing Helper Contract

We charge 0.01 BOBA for each Turing request and it's based on the Turing Helper Contract.
1
// Deploy Turing Helper Contract
2
const Factory__TuringHelperr = new ContractFactory(
3
TuringHelperJson.abi,
4
TuringHelperJson.bytecode,
5
(hre as any).deployConfig.deployer_l2
6
)
7
8
const TuringHelper = await Factory__TuringHelperr.deploy()
9
await TuringHelper.deployTransaction.wait()
10
11
console.log(`TuringHelper was deployed at: ${TuringHelper.address}`)
12
13
// Approve Boba Token to add funds to Turing Credit Contract
14
const BobaTuringCredit = new Contract(
15
(hre as any).deployConfig.BobaTuringCreditAddress,
16
BobaTuringCreditJson.abi,
17
(hre as any).deployConfig.deployer_l2
18
)
19
const approveTx = await L2BobaToken.approve(
20
BobaTuringCredit.address,
21
utils.parseEther('100')
22
)
23
await approveTx.wait()
24
25
const addCreditTx = await BobaTuringCredit.addBalanceTo(
26
utils.parseEther('100'),
27
TuringHelper.address
28
)
29
await addCreditTx.wait()
30
31
// Add permission for Boba Faucet contract
32
const addPermissionTx = await TuringHelper.addPermittedCaller(
33
BobaFaucet.address
34
)
35
await addPermissionTx.wait()
Copied!