description |
---|
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.
boba_community/turing-captcha-faucet/packages
: Contains all the typescript packages and contractscontracts
: Smart contracts implementing the Boba Faucetgateway
: The Boba Web faucet frontenddeployment
: Boba faucet Rinkeby contract addressesapi
: Boba faucet backend API
The token-claiming process takes place in five steps:
The API GET request is sent to https://api-turing.boba.network/get.catcha
on the frontend. The returned payload is
{
"UUID": "BYTES32",
"imageBase64": "CAPTCHA IMAGE"
}
The UUID and hashed CAPTCHA answer are stored in AWS Redis.
const BobaFaucet = new ethers.Contract(
BOBA_FAUCET_CONTRACE_ADDRESS,
BobaFaucetJson.abi,
this.provider.getSigner()
)
const tx = await BobaFaucet.getBobaFaucet(uuid, answer)
await tx.wait()
The answer is hashed in the Boba Faucet
contract first before sending it to backend API.
bytes32 hashedKey = keccak256(abi.encodePacked(_key));
bytes memory encRequest = abi.encodePacked(_uuid, hashedKey);
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
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.
paramsHexString = body['params'][0]
# Select uuid and answer
# paramsHexString[0: 66] is the length of input data
uuid = '0x' + paramsHexString[66: 130]
answer = '0x' + paramsHexString[130:]
# Get answer from Redis
keyInRedis = db.get(uuid)
# Return the payload
return returnPayload(keyInRedis.decode('utf-8') == key)
# Payload is built based on the result
def returnPayload(result):
# We return 0 or 1 using uint256
payload = '0x' + '{0:0{1}x}'.format(int(32), 64)
if result:
# Add UINT256 1 if the result is correct
payload += '{0:0{1}x}'.format(int(1), 64)
else:
# Add UINT256 0 if the result is wrong
payload += '{0:0{1}x}'.format(int(0), 64)
returnPayload = {
'statusCode': 200,
'body': json.dumps({ 'result': payload })
}
return returnPayload
On the contract level, we decode the result from the Turing request and release the funds if the answer is correct.
// Decode the response from outside API
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
uint256 result = abi.decode(encResponse,(uint256));
// Release the funds if it is correct
require(result == 1, 'Captcha wrong');
IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);
Two simple API endpoints are created.
image = ImageCaptcha(width=280, height=90)
# For image
uuid1 = uuid.uuid4()
imageStr = str(uuid1).split('-')[0]
imageStrBytes = Web3.solidityKeccak(['string'], [str(imageStr)]).hex()
# For key
uuid2 = uuid.uuid4()
keyBytes = Web3.solidityKeccak(['string'], [str(uuid2)]).hex()
# Use the first random string
imageData = image.generate(imageStr)
# Get base64
imageBase64 = base64.b64encode(imageData.getvalue())
# Read YML
with open("env.yml", 'r') as ymlfile:
config = yaml.load(ymlfile)
# Connect to Redis Elasticache
db = redis.StrictRedis(host=config.get('REDIS_URL'),
port=config.get('REDIS_PORT'))
# Store and set the expire time as 10 minutes
print('keyBytes: ', keyBytes, 'imageStrBytes: ', imageStrBytes, 'imageStr: ', imageStr)
db.set(keyBytes, imageStrBytes, ex=600)
payload = {'uuid': keyBytes, 'imageBase64': imageBase64.decode('utf-8') }
paramsHexString = body['params'][0]
uuid = '0x' + paramsHexString[66: 130]
key = '0x' + paramsHexString[130:]
# Read YML
with open("env.yml", 'r') as ymlfile:
config = yaml.load(ymlfile)
# Connect to Redis Elasticache
db = redis.StrictRedis(host=config.get('REDIS_URL'),
port=config.get('REDIS_PORT'))
# Store and set the expire time as 10 minutes
keyInRedis = db.get(uuid)
if keyInRedis:
print('keyInRedis: ', keyInRedis.decode('utf-8'), 'key: ', key)
return returnPayload(keyInRedis.decode('utf-8') == key)
else:
return returnPayload(False)
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
.
NETWORK=rinkeby
L1_NODE_WEB3_URL=https://rinkeby.infura.io/v3/9844f35ff4a84003a7025a65a9412002
L2_NODE_WEB3_URL=https://rinkeby.boba.network
ADDRESS_MANAGER_ADDRESS=0x93A96D6A5beb1F661cf052722A1424CDDA3e9418
DEPLOYER_PRIVATE_KEY=
Build and deploy all the needed contracts:
$ yarn build
$ yarn deploy
The smart contract imports the Turing Helper Contract so it can interact with outside API endpoints.
import './TuringHelper.sol';
contract BobaFaucet is Ownable {
// Turing
address public turingHelperAddress;
string public turingUrl;
TuringHelper public turing;
constructor(
address _turingHelperAddress,
string memory _turingUrl
) {
turingHelperAddress = _turingHelperAddress;
turing = TuringHelper(_turingHelperAddress);
turingUrl = _turingUrl;
}
function getBobaFaucet(
bytes32 _uuid,
string memory _key
) external {
require(BobaClaimRecords[msg.sender] + waitingPeriod < block.timestamp, 'Invalid request');
// The key is hashed
bytes32 hashedKey = keccak256(abi.encodePacked(_key));
uint256 result = _verifyKey(_uuid, hashedKey);
require(result == 1, 'Captcha wrong');
BobaClaimRecords[msg.sender] = block.timestamp;
IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);
emit GetBobaFaucet(_uuid, hashedKey, msg.sender, BobaFaucetAmount, block.timestamp);
}
// Call Turing contract to get the result from the outside API endpoint
function _verifyKey(bytes32 _uuid, bytes32 _key) private returns (uint256) {
bytes memory encRequest = abi.encodePacked(_uuid, _key);
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
uint256 result = abi.decode(encResponse,(uint256));
emit VerifyKey(turingUrl, _uuid, _key, result);
return result;
}
}
We charge 0.01 BOBA for each Turing request and it's based on the Turing Helper Contract.
// Deploy Turing Helper Contract
const Factory__TuringHelperr = new ContractFactory(
TuringHelperJson.abi,
TuringHelperJson.bytecode,
(hre as any).deployConfig.deployer_l2
)
const TuringHelper = await Factory__TuringHelperr.deploy()
await TuringHelper.deployTransaction.wait()
console.log(`TuringHelper was deployed at: ${TuringHelper.address}`)
// Approve Boba Token to add funds to Turing Credit Contract
const BobaTuringCredit = new Contract(
(hre as any).deployConfig.BobaTuringCreditAddress,
BobaTuringCreditJson.abi,
(hre as any).deployConfig.deployer_l2
)
const approveTx = await L2BobaToken.approve(
BobaTuringCredit.address,
utils.parseEther('100')
)
await approveTx.wait()
const addCreditTx = await BobaTuringCredit.addBalanceTo(
utils.parseEther('100'),
TuringHelper.address
)
await addCreditTx.wait()
// Add permission for Boba Faucet contract
const addPermissionTx = await TuringHelper.addPermittedCaller(
BobaFaucet.address
)
await addPermissionTx.wait()