How to Build a Token Authority?

Prerequisites


As mentioned in the previous page, the Token Authority is another smart contract which protects your primary smart contract. The parameters which are required during the deployment are the address of the initialOwner.

On the date of writing this document, the version of Solidity for deploying smart contracts on Oasis must be 0.8.24.

This code block is an example of simple Token Authority smart contract.

Please, search for comment that says CODE HERE for configuring this Token Authority to match with your needs.

// 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";

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();

        // CODE HERE
        // Replace with actual kernel ids

        // kernels[XXXXXX] = true;
        // kernels[YYYYYY] = true;
        // kernels[ZZZZZZ] = true;
        // kernels[123123] = true;

        whitelist[address(0xc770EAc29244C1F88E14a61a6B99d184bfAe93f5)] = true;
        runtimeDigests[
            0x876924e18dd46dd3cbcad570a87137bbd828a7d0f3cad309f78ad2c9402eeeb7
        ] = 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++) {

            // CODE HERE
            // Example methods of handling responses from kernels

            // // If response is in uint type
            // if (_executions[i].kernelId == REPLACE_WITH_KERNEL_ID) {
            //     uint256 result = abi.decode(_executions[i].result, (uint256));
            //     if (result > 0) {
            //         _executions[i].opinion = true;
            //         _executions[i].opinionDetails = "All good";
            //         _executions[i].isValidated = true;
            //     }
            // }

            // // If response is in boolean type
            // if (_executions[i].kernelId == REPLACE_WITH_KERNEL_ID) {
            //     bool result = abi.decode(_executions[i].result, (bool));
            //     // Compare true or false
            //     // if (result == true) {
            //     // if (result == false) {
            //         _executions[i].opinion = true;
            //         _executions[i].opinionDetails = "All good";
            //         _executions[i].isValidated = true;
            //     }
            // }

            // // If response needs to be parsed
            // if (_executions[i].kernelId == REPLACE_WITH_KERNEL_ID) {
            //     _executions[i].opinion = true;
            //     _executions[i].opinionDetails = "All good";
            //     _executions[i].isValidated = 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) {
        return kernels[kernelId];
    }

    // 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
        );
    }
}

Last updated