Speed's Workshop
Last updated
Was this helpful?
Last updated
Was this helpful?
The content of this page may be updated over time. Therefore, it is more important to understand how kOS functions than to remember the specific steps for using our technology from this page.
Sepolia
Oasis Sapphire testnet
Arbitrum Sepolia
SepoliaETH token ≈ 0.1 SepoliaETH
Oasis Sapphire testnet token ≈ 0.6 TEST
Arbitrum Sepolia token ≈ 0.1 ETH (or else)
Speed will walk through the overall setup.
This guide focuses on how to secure the bounty reward. *wink wink* 😉😉
Information for Remix verification plug-in
API URL
https://api-sepolia.arbiscan.io/
Explorer URL
https://sepolia.arbiscan.io
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
contract Exchange {
// From 1st of April 2025
uint256 ethToUsdc = 1834; // static value, the real oracle exchange rate would be dynamic based on time
function getExchangeRate() view external returns (uint256) {
// This is just a mocking code for fetching data from an oracle source.
// The actual production grade code would have more security checks/layers.
// However, in this workshop, we are going to simplify pretty much all the logics of oracle code.
// Things would then be cut-down into just returning the exchange rate.
return ethToUsdc;
}
}
In this workshop, we will use Arbitrum Sepolia (chain ID 421614) network.
You may choose to deploy on other networks such as Base Sepolia, Optimism Sepolia, or Sepolia.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
contract Allowlist {
// Declare mapping in Solidity
// Some people might call this mapping as:
// dict (Python)
// map (JavaScript)
// hash (Ruby)
mapping (address => bool) private allowMapping;
// "checkAllowlist" will be registered as on-chain kernel
function checkAllowlist(address walletAddressInput) view external returns (bool) {
// Simple logic to check if your wallet address is in Allowlist or not
if (allowMapping[walletAddressInput] == true) {
return true;
} else {
return false;
}
}
// For registering yourself to be in Allowlist
function registerAllowlist() external {
allowMapping[msg.sender] = true;
}
}
In this workshop, we will use Arbitrum Sepolia (chain ID 421614) network.
You may choose to deploy on other networks such as Base Sepolia, Optimism Sepolia, or Sepolia.
https://sepolia.arbiscan.io/address/0x123
git clone https://github.com/KRNL-Labs/krnl-toolkit.git
ETHERSCAN_API_KEY= # REQUIRED
# PRIVATE KEYS; they can be the same private key
PRIVATE_KEY_OASIS= # REQUIRED; no need 0x prefix, can be the same as Sepolia
PRIVATE_KEY_SEPOLIA= # REQUIRED; no need 0x prefix, can be the same as Oasis
# SELECTED KERNEL IDS FOR REGISTERING SMART CONTRACT
# KERNEL_ID=337, 123, 456
KERNEL_ID=337 <<<<<<<<<<<<<< CHANGE THIS TO BE THE ONES THAT YOU REGISTERED
# OPTIONAL
INFURA_PROJECT_ID= # OPTIONAL FOR SEPOLIA
Directory: krnl-toolkit/smart-contracts/hardhat/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
import "@oasisprotocol/sapphire-contracts/contracts/EthereumUtils.sol";
// ===============================
// Search for "HERE"
// ===============================
contract TokenAuthority is Ownable {
Keypair private signingKeypair;
Keypair private accessKeypair;
bytes32 private signingKeypairRetrievalPassword;
// https://api.docs.oasis.io/sol/sapphire-contracts/contracts/Sapphire.sol/library.Sapphire.html#secp256k1--secp256r1
struct Keypair {
bytes pubKey;
bytes privKey;
}
struct Execution {
uint256 kernelId;
bytes result;
bytes proof;
bool isValidated;
bool opinion;
string opinionDetails;
string err;
}
mapping(address => bool) private whitelist; // krnlNodePubKey to bool
mapping(bytes32 => bool) private runtimeDigests; // runtimeDigest to bool
mapping(uint256 => bool) private kernels; // kernelId to bool
constructor(address initialOwner) Ownable(initialOwner) {
signingKeypair = _generateKey();
accessKeypair = _generateKey();
// HERE
// Set allowed kernel(s)
// kernels[REPLACE_WITH_KERNEL_ID] = true;
// kernels[REPLACE_WITH_KERNEL_ID] = true
// Set node whitelist
whitelist[address(0xc770EAc29244C1F88E14a61a6B99d184bfAe93f5)] = true;
// Set runtime digest
runtimeDigests[
0x876924e18dd46dd3cbcad570a87137bbd828a7d0f3cad309f78ad2c9402eeeb7
] = true;
// v0.0.3
runtimeDigests[
0xa02c15786858a1b8ac0c421f451b5dc0e5e370c4c1d738a9fc9c1c141979da21
] = true;
}
modifier onlyAuthorized(bytes calldata auth) {
(
bytes32 entryId,
bytes memory accessToken,
bytes32 runtimeDigest,
bytes memory runtimeDigestSignature,
uint256 nonce,
uint256 blockTimeStamp,
bytes memory authSignature
) = abi.decode(
auth,
(bytes32, bytes, bytes32, bytes, uint256, uint256, bytes)
);
require(_verifyAccessToken(entryId, accessToken));
_;
}
modifier onlyValidated(bytes calldata executionPlan) {
require(_verifyExecutionPlan(executionPlan));
_;
}
modifier onlyAllowedKernel(uint256 kernelId) {
require(kernels[kernelId]);
_;
}
function _validateExecution(
bytes calldata executionPlan
) external view returns (bytes memory) {
Execution[] memory _executions = abi.decode(
executionPlan,
(Execution[])
);
for (uint256 i = 0; i < _executions.length; i++) {
// HERE
_executions[i].isValidated = true;
_executions[i].opinion = true;
}
return abi.encode(_executions);
}
function _generateKey() private view returns (Keypair memory) {
bytes memory seed = Sapphire.randomBytes(32, "");
(bytes memory pubKey, bytes memory privKey) = Sapphire
.generateSigningKeyPair(
Sapphire.SigningAlg.Secp256k1PrehashedKeccak256,
seed
);
return Keypair(pubKey, privKey);
}
function _verifyAccessToken(
bytes32 entryId,
bytes memory accessToken
) private view returns (bool) {
bytes memory digest = abi.encodePacked(keccak256(abi.encode(entryId)));
return
Sapphire.verify(
Sapphire.SigningAlg.Secp256k1PrehashedKeccak256,
accessKeypair.pubKey,
digest,
"",
accessToken
);
}
function _verifyRuntimeDigest(
bytes32 runtimeDigest,
bytes memory runtimeDigestSignature
) private view returns (bool) {
address recoverPubKeyAddr = ECDSA.recover(
runtimeDigest,
runtimeDigestSignature
);
return whitelist[recoverPubKeyAddr];
}
function _verifyExecutionPlan(
bytes calldata executionPlan
) private pure returns (bool) {
Execution[] memory executions = abi.decode(
executionPlan,
(Execution[])
);
for (uint256 i = 0; i < executions.length; i++) {
if (!executions[i].isValidated) {
return false;
}
}
return true;
}
function _getFinalOpinion(
bytes calldata executionPlan
) private pure returns (bool) {
Execution[] memory executions = abi.decode(
executionPlan,
(Execution[])
);
for (uint256 i = 0; i < executions.length; i++) {
if (!executions[i].opinion) {
return false;
}
}
return true;
}
function setSigningKeypair(
bytes calldata pubKey,
bytes calldata privKey
) external onlyOwner {
signingKeypair = Keypair(pubKey, privKey);
}
function setSigningKeypairRetrievalPassword(
string calldata _password
) external onlyOwner {
signingKeypairRetrievalPassword = keccak256(
abi.encodePacked(_password)
);
}
function getSigningKeypairPublicKey()
external
view
returns (bytes memory, address)
{
address signingKeypairAddress = EthereumUtils
.k256PubkeyToEthereumAddress(signingKeypair.pubKey);
return (signingKeypair.pubKey, signingKeypairAddress);
}
function getSigningKeypairPrivateKey(
string calldata _password
) external view onlyOwner returns (bytes memory) {
require(
signingKeypairRetrievalPassword ==
keccak256(abi.encodePacked(_password))
);
return signingKeypair.privKey;
}
function setWhitelist(
address krnlNodePubKey,
bool allowed
) external onlyOwner {
whitelist[krnlNodePubKey] = allowed;
}
function setRuntimeDigest(
bytes32 runtimeDigest,
bool allowed
) external onlyOwner {
runtimeDigests[runtimeDigest] = allowed;
}
function setKernel(uint256 kernelId, bool allowed) external onlyOwner {
kernels[kernelId] = allowed;
}
function registerdApp(
bytes32 entryId
) external view returns (bytes memory) {
bytes memory digest = abi.encodePacked(keccak256(abi.encode(entryId)));
bytes memory accessToken = Sapphire.sign(
Sapphire.SigningAlg.Secp256k1PrehashedKeccak256,
accessKeypair.privKey,
digest,
""
);
return accessToken;
}
function isKernelAllowed(
bytes calldata auth,
uint256 kernelId
) external view onlyAuthorized(auth) returns (bool) {
// HERE
// All kernels are allowed
// Not recommended for production
return true;
}
// example use case: only give 'true' opinion when all kernels are executed with expected results and proofs
function getOpinion(
bytes calldata auth,
bytes calldata executionPlan
) external view onlyAuthorized(auth) returns (bytes memory) {
try this._validateExecution(executionPlan) returns (
bytes memory result
) {
return result;
} catch {
return executionPlan;
}
}
function sign(
bytes calldata auth,
address senderAddress,
bytes calldata executionPlan,
bytes calldata functionParams,
bytes calldata kernelParams,
bytes calldata kernelResponses
)
external
view
onlyValidated(executionPlan)
onlyAuthorized(auth)
returns (bytes memory, bytes32, bytes memory, bool)
{
(
bytes32 id,
bytes memory accessToken,
bytes32 runtimeDigest,
bytes memory runtimeDigestSignature,
uint256 nonce,
uint256 blockTimeStamp,
bytes memory authSignature // id, accessToken, runtimeDigest, runtimeDigestSignature, nonce, blockTimeStamp, authSignature
) = abi.decode(
auth,
(bytes32, bytes, bytes32, bytes, uint256, uint256, bytes)
);
// Compute kernelResponsesDigest
bytes32 kernelResponsesDigest = keccak256(
abi.encodePacked(kernelResponses, senderAddress)
);
bytes memory kernelResponsesSignature = Sapphire.sign(
Sapphire.SigningAlg.Secp256k1PrehashedKeccak256,
signingKeypair.privKey,
abi.encodePacked(kernelResponsesDigest),
""
);
(, SignatureRSV memory kernelResponsesRSV) = EthereumUtils
.toEthereumSignature(
signingKeypair.pubKey,
kernelResponsesDigest,
kernelResponsesSignature
);
bytes memory kernelResponsesSignatureEth = abi.encodePacked(
kernelResponsesRSV.r,
kernelResponsesRSV.s,
uint8(kernelResponsesRSV.v)
);
bytes32 functionParamsDigest = keccak256(functionParams);
// Compute kernelParamsDigest
bytes32 kernelParamsDigest = keccak256(
abi.encodePacked(kernelParams, senderAddress)
);
bool finalOpinion = _getFinalOpinion(executionPlan);
// Compute dataDigest
bytes32 dataDigest = keccak256(
abi.encodePacked(
functionParamsDigest,
kernelParamsDigest,
senderAddress,
nonce,
finalOpinion
)
);
bytes memory signature = Sapphire.sign(
Sapphire.SigningAlg.Secp256k1PrehashedKeccak256,
signingKeypair.privKey,
abi.encodePacked(dataDigest),
""
);
(, SignatureRSV memory rsv) = EthereumUtils.toEthereumSignature(
signingKeypair.pubKey,
dataDigest,
signature
);
bytes memory signatureToken = abi.encodePacked(
rsv.r,
rsv.s,
uint8(rsv.v)
);
return (
kernelResponsesSignatureEth,
kernelParamsDigest,
signatureToken,
finalOpinion
);
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import {KRNL, KrnlPayload, KernelParameter, KernelResponse} from "./KRNL.sol";
// This contract uses Mocking ETH and Mocking USDC
// The Mocking ETH and Mocking USDC are just numbers in this contact
// You will not lose any real ETH or USDC by using this contract
// The tokens are not ERC-20, they are just numbers in this contract
// In order to submit our bounty reward, you may take this contract as an example
// However, the code will need to be more robust and secure
contract Sample is KRNL {
// Token Authority public key as a constructor
constructor(address _tokenAuthorityPublicKey) KRNL(_tokenAuthorityPublicKey) {}
// Results from kernel will be emitted through this event
event Broadcast(address sender, uint256 exchangeRate, bool allowlist);
// Mapping to store the balance of Mocking USDC for wallet address
mapping (address => uint256) public balanceList;
// Protected function
function protectedFunction(
KrnlPayload memory krnlPayload,
uint256 input
)
external
onlyAuthorized(krnlPayload, abi.encode(input))
{
// Decode response from kernel
KernelResponse[] memory kernelResponses = abi.decode(krnlPayload.kernelResponses, (KernelResponse[]));
// Response variables for kernels
uint256 exchangeRate;
bool allowlist;
// Decoding exchange rate and allowlist from kernels
for (uint i; i < kernelResponses.length; i ++) {
// HERE
if (kernelResponses[i].kernelId == REPLACE_WITH_EXCHANGE_RATE_KERNEL_ID) {
exchangeRate = abi.decode(kernelResponses[i].result, (uint256));
}
// HERE
if (kernelResponses[i].kernelId == REPLACE_WITH_ALLOWLIST_KERNEL_ID) {
allowlist = abi.decode(kernelResponses[i].result, (bool));
}
}
// End of decoding kernel responses
// Checking if wallet address is in allowlist or not
if (allowlist == true) {
// If yes, the Mocking ETH token will be converted to Mocking USDC
balanceList[msg.sender] = balanceList[msg.sender] + (input * exchangeRate);
}
// If wallet address is not in Allowlist, we still emit the event
// The kernel responses can still be seen in the event logs
emit Broadcast(msg.sender, exchangeRate, allowlist);
}
// Function to see the balance of Mocking USDC
function seeUsdcBalance(address walletAddressToCheckBalance) external view returns (uint256) {
return balanceList[walletAddressToCheckBalance];
}
}
npm install
npm run hdvr
...
...
...
======================================
=====SUMMARY=====
Registered Smart Contract ID: 789789
dApp ID: 123456
Please visit this page for Entry ID, Access Token, and Kernel Payload
https://app.platform.lat/dapp/123456
Tips 1: Entry ID and Access Token are similar to x-api-key or Bearer Token of Web2
Tips 2: Kernel Payload is the template of parameter(s) that needs to be sent to kernel ID(s): [ 1111, 2222 ]
======================================
npx create-next-app@latest
npm install krnl-sdk
'use client'
import { ethers } from "krnl-sdk";
import { useState } from "react";
export default function Home() {
const entryId = "REPLACE_WITH_ENTRY_ID"
const accessToken = "REPLACE_WITH_ACCESS_TOKEN"
const deployedContractAddress = "REPLACE_WITH_DEPLOYED_CONTRACT_ADDRESS"
const exchangeKernelId = "REPLACE_WITH_EXCHANGE_KERNEL_ID"
const allowlistKernelId = "REPLACE_WITH_ALLOWLIST_KERNEL_ID"
const amountOfEthToConvertToUsdc = 10 // or any amount that you need
const abiCoder = new ethers.AbiCoder();
const [isConnected, setIsConnected] = useState(false);
const [signer, setSigner] = useState<ethers.JsonRpcSigner | null>(null);
const [walletAddress, setWalletAddress] = useState<string | null>(null);
const [transactionHash, setTransactionHash] = useState<string | null>(null);
const [transactionAttempted, setTransactionAttempted] = useState(false);
const [chainId, setChainId] = useState<bigint | null>(null);
const [encodedWalletAddress, setEncodedWalletAddress] = useState<string | null>(null);
const connectWallet = async () => {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const signerAddress = await signer.getAddress();
const chainId = await provider.getNetwork();
const encodedWalletAddress = abiCoder.encode(["address"], [`${signerAddress}`]);
setChainId(chainId.chainId);
setIsConnected(true);
setSigner(signer);
setWalletAddress(signerAddress);
setEncodedWalletAddress(encodedWalletAddress);
}
const disconnectWallet = async () => {
setIsConnected(false);
setSigner(null);
setWalletAddress(null);
setChainId(null);
setEncodedWalletAddress(null);
}
const functionParams = abiCoder.encode(["uint256"], [amountOfEthToConvertToUsdc]);
const krnlNodeProvider = new ethers.JsonRpcProvider("https://v0-0-3-rpc.node.lat");
const functionInterface = new ethers.Interface([
"function protectedFunction(tuple(bytes auth, bytes kernelResponses, bytes kernelParams),uint256)"
]);
const makeTransaction = async () => {
setTransactionAttempted(true);
if (!signer) {
console.log("No signer found");
return;
}
console.log("STARTING MAKING TRANSACTION")
const kernelRequestData: any = {
"senderAddress": `${walletAddress}`,
"kernelPayload": {
[allowlistKernelId]: {
"functionParams": `${encodedWalletAddress}`
},
[exchangeKernelId]: {
"functionParams": ""
}
}
}
console.log("CALLING executeKernels")
const executeResult = await krnlNodeProvider.executeKernels(entryId,
accessToken,
kernelRequestData,
functionParams);
console.log("SUCCESS executeKernels")
const krnlPayload = [
executeResult.auth,
executeResult.kernel_responses,
executeResult.kernel_params
];
console.log("CALLING TO SMART CONTRACT")
const txPayload = functionInterface.encodeFunctionData("protectedFunction", [krnlPayload, 10]);
const tx = await signer.sendTransaction({
to: deployedContractAddress,
data: txPayload
});
const transactionHash = await tx.wait();
console.log("TRANSACTION FINISHED")
setTransactionHash(transactionHash?.hash || null);
}
return (
<div>
<h1>dApp template</h1>
<br/>
<h1>
Wallet: {isConnected ? walletAddress : "Not Connected"}
</h1>
<br/>
<h1>
Chain ID: {chainId ? chainId : "Not Connected"}
</h1>
<br/>
<button onClick={isConnected ? disconnectWallet : connectWallet}>
{isConnected ? "Disconnect Wallet" : "Connect Wallet"}
</button>
<br/>
<br/>
<button onClick={makeTransaction}>Make Transaction</button>
{transactionAttempted && (
<h1>
Transaction Hash: {transactionHash ? transactionHash : "Making Transaction..."}
</h1>
)}
</div>
)
}
npm run dev
Custom - User Defined Contract
Custom - User Defined Contract
Smart Contract Address
YOUR_SMART_CONTRACT_ADDRESS
Function Signature
protectedFunction(KrnlPayload,uint256)
RpcEndpoint
https://v0-0-3-rpc.node.lat
EntryID
YOUR_ENTRY_ID
Access Token
YOUR_ACCESS_TOKEN
Kernel Request Data
{
"senderAddress": "0xYOUR_WALLET_ADDRESS",
"kernelPayload": {
"CHANGE_TO_ALLOWLIST_KERNEL_ID": {
"functionParams": "0x000000000000000000000000REPLACE_WITH_WALLET_ADDRESS_NO_0x"
},
"CHANGE_TO_EXCHANGE_RATE_KERNEL_ID": {
"functionParams": ""
}
}
}
Function Params
Any positive number
For integrating Web API with kOS
For other on-chain kernels, the supported blockchain networks (current):
Ethereum (mainnet)
Sepolia
Base
Base Sepolia
Optimism
Optimism Sepolia
Arbitrum
Arbitrum Sepolia
Decoding different data types (from kernel responses) with Solidity
Already have your own Solidity smart contract?
You may use this idea to apply for bounty.
This entire kOS flow is not good because:
Exchange rate kernel (oracle) is static
Allowlist kernel (KYC) does not contain any security check/proof
Token Authority allows all kernels without any restriction
Smart contract (Sample.sol) convert numbers and not ERC-20 token(s)
dApp is very static (fixed amount of Mock ETH to convert)
dApp's interface looks plain
There might be some mistakes/vulnerabilities in some part of the code