Precompile

A precompile is useful if you need the fully assembled binary representation of a TEAL program in the logic of another TEAL program.

One example of where it might be used, is in offloading some compute into a LogicSignature, which has a max opcode budget of 20k ops compared with 700 ops in a single Application call.

By using the precompile in this way we can check offload the more expensive computation to the LogicSignature and preserve the app calls opcode budget for other work.

Note

When we do this, we need to be careful that the LogicSignature is the one we expect by checking its address, which is the hash of the program logic.

Another example might be if you need to deploy some child Application and want to have the logic locally in the program as compiled bytes so the “parent app” can create the “child app” directly in program logic.

By using the precompile in this way, we can deploy the Sub Application directly from our Parent app.

Note

When we do this, the compiled program will take up more space in our “parent app” contract, so some consideration of the trade offs between program size and convenience may be necessary.

Usage

In order to use a Precompile in a program, first wrap the LogicSignature or Application with the precompile method. This will ensure that the program is fully compiled once and only once and the binary versions of the assembled programs are available when it’s time to build the containing Application or LogicSignature.

Note

The precompile function may _only_ be called inside a function.

@app.external
def create_child_1(*, output: pt.abi.Uint64) -> pt.Expr:
    """Create a new child app."""
    child1_pc = beaker.precompiled(child1.app)
    return pt.Seq(
        pt.InnerTxnBuilder.Execute(child1_pc.get_create_config()),
        output.set(pt.InnerTxn.created_application_id()),
    )

Reference

class beaker.precompile.PrecompiledApplication[source]

AppPrecompile allows a smart contract to signal that some child Application should be fully compiled prior to constructing its own program.

__init__(app: Application, client: AlgodClient)[source]
get_create_config() dict[pyteal.TxnField, pyteal.Expr | list[pyteal.Expr]][source]

get a dictionary of the fields and values that should be set when creating this application that can be passed directly to the InnerTxnBuilder.Execute method

class beaker.precompile.PrecompiledLogicSignature[source]

LSigPrecompile allows a smart contract to signal that some child Logic Signature should be fully compiled prior to constructing its own program.

__init__(lsig: LogicSignature, client: AlgodClient)[source]
address() Expr[source]

Get the address from this LSig program.

class beaker.precompile.PrecompiledLogicSignatureTemplate[source]

LSigPrecompile allows a smart contract to signal that some child Logic Signature should be fully compiled prior to constructing its own program.

__init__(lsig: LogicSignatureTemplate, client: AlgodClient)[source]
address(**kwargs: Expr) Expr[source]

returns an expression that will generate the expected hash given some set of values that should be included in the logic itself

populate_template(**kwargs: str | bytes | int) bytes[source]

populate_template returns the bytes resulting from patching the set of arguments passed into the blank binary

The args passed should be of the same type and in the same order as the template values declared.

populate_template_expr(**kwargs: Expr) Expr[source]

populate_template_expr returns the Expr that will patch a blank binary given a set of arguments.

It is called by address to return an Expr that can be used to compare with a sender given some arguments.

Examples

Using Precompile for offloading compute

from typing import Literal

import pyteal as pt

import beaker


@pt.Subroutine(pt.TealType.uint64)
def eth_ecdsa_validate(hash_value: pt.Expr, signature: pt.Expr) -> pt.Expr:
    """
    Return a 1/0 for valid signature given hash

    Equivalent of OpenZeppelin ECDSA.recover for long 65-byte Ethereum signatures
    https://docs.openzeppelin.com/contracts/2.x/api/cryptography#ECDSA-recover-bytes32-bytes-
    Short 64-byte Ethereum signatures require some changes to the code


    [1] https://github.com/OpenZeppelin/openzeppelin-contracts/blob/5fbf494511fd522b931f7f92e2df87d671ea8b0b/contracts/utils/cryptography/ECDSA.sol#L153


    Note: Unless compatibility with Ethereum or another system is necessary,
    we highly recommend using ed25519_verify instead of ecdsa on Algorand

    WARNING: This code has NOT been audited
    DO NOT USE IN PRODUCTION
    """

    r = pt.Extract(signature, pt.Int(0), pt.Int(32))
    s = pt.Extract(signature, pt.Int(32), pt.Int(32))

    # The recovery ID is shifted by 27 on Ethereum
    # For non-Ethereum signatures, remove the -27 on the line below
    v = pt.Btoi(pt.Extract(signature, pt.Int(64), pt.Int(1))) - pt.Int(27)

    return pt.Seq(
        pt.Assert(
            pt.Len(signature) == pt.Int(65),
            pt.Len(hash_value) == pt.Int(32),
            # The following two asserts are to prevent malleability like in [1]
            pt.BytesLe(
                s,
                pt.Bytes(
                    "base16",
                    "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0",
                ),
            ),
            v <= pt.Int(1),
        ),
        pt.EcdsaVerify(
            pt.EcdsaCurve.Secp256k1,
            hash_value,
            r,
            s,
            pt.EcdsaRecover(pt.EcdsaCurve.Secp256k1, hash_value, v, r, s),
        ),
    )


verify_lsig = beaker.LogicSignature(
    pt.Seq(
        pt.Assert(
            # Don't let it be rekeyed
            pt.Txn.rekey_to() == pt.Global.zero_address(),
            # Don't take a fee from me
            pt.Txn.fee() == pt.Int(0),
            # Make sure I've signed an app call
            pt.Txn.type_enum() == pt.TxnType.ApplicationCall,
            # Make sure I have the args I expect [method_selector, hash_value, signature]
            pt.Txn.application_args.length() == pt.Int(3),
        ),
        eth_ecdsa_validate(pt.Txn.application_args[1], pt.Txn.application_args[2]),
    ),
    build_options=beaker.BuildOptions(avm_version=6),
)
"""
This LogicSignature takes two application arguments:
  hash, signature
and returns the validity of the signature given the hash
as written in OpenZeppelin https://docs.openzeppelin.com/contracts/2.x/api/cryptography#ECDSA-recover-bytes32-bytes-
(65-byte signatures only)
"""

app = beaker.Application("EthChecker")


@app.external
def check_eth_sig(
    hash: pt.abi.StaticBytes[Literal[32]],
    signature: pt.abi.StaticBytes[Literal[65]],
    *,
    output: pt.abi.String
) -> pt.Expr:
    # The lsig that will be responsible for validating the
    # incoming signature against the incoming hash
    # When passed to Precompile, it flags the init of the Application
    # to prevent building approval/clear programs until the precompile is
    # compiled so we have access to compiled information (its address for instance)
    verifier = beaker.precompiled(verify_lsig)

    return pt.Seq(
        # The precompiled lsig should have its address and binary available
        # here so we can use it to make sure we've been called
        # with the correct lsig
        pt.Assert(pt.Txn.sender() == verifier.address()),
        output.set("lsig validated"),
    )

Using Precompile for a child Application

import pyteal as pt

import beaker

from examples.nested_precompile.smart_contracts import child1, child2

app = beaker.Application("Parent")


@app.external
def create_child_1(*, output: pt.abi.Uint64) -> pt.Expr:
    """Create a new child app."""
    child1_pc = beaker.precompiled(child1.app)
    return pt.Seq(
        pt.InnerTxnBuilder.Execute(child1_pc.get_create_config()),
        output.set(pt.InnerTxn.created_application_id()),
    )


@app.external
def create_child_2(*, output: pt.abi.Uint64) -> pt.Expr:
    """Create a new child app."""
    child2_pc = beaker.precompiled(child2.app)
    return pt.Seq(
        pt.InnerTxnBuilder.Execute(
            {
                **child2_pc.get_create_config(),
                pt.TxnField.global_num_uints: pt.Int(1),  # override because..?
            }
        ),
        output.set(pt.InnerTxn.created_application_id()),
    )