Skip to Content
Scalus Club is now open! Join us to get an early access to new features 🎉

Builtin Functions

Cardano Plutus provides built-in functions optimized for blockchain execution. Scalus exposes these functions through the Builtins object with familiar Scala syntax.

These builtin functions are automatically recognized by the Scalus compiler and compiled to their corresponding Plutus operations for efficient on-chain execution.

Plutus Versions

Builtin functions are available in different Plutus versions:

  • Plutus V1 - Original builtins (integers, bytestrings, lists, data, basic crypto)
  • Plutus V2 - Added serialiseData, SECP256k1 signatures
  • Plutus V3 - Added BLS12-381, Keccak-256, bitwise operations
  • Plutus V4 - Added array operations, dropList, ripemd_160, BuiltinValue operations

Integer Operations

Arithmetic and comparison operations on arbitrary-precision integers (BigInt).

import scalus.builtin.Builtins // Arithmetic val sum = Builtins.addInteger(BigInt(10), BigInt(20)) val diff = Builtins.subtractInteger(BigInt(100), BigInt(30)) val product = Builtins.multiplyInteger(BigInt(5), BigInt(7)) val quotient = Builtins.divideInteger(BigInt(20), BigInt(4)) // floor division val quot = Builtins.quotientInteger(BigInt(20), BigInt(4)) // truncated division val remainder = Builtins.remainderInteger(BigInt(17), BigInt(5)) val modulo = Builtins.modInteger(BigInt(17), BigInt(5)) // Comparison val eq = Builtins.equalsInteger(BigInt(42), BigInt(42)) val lt = Builtins.lessThanInteger(BigInt(3), BigInt(7)) val lte = Builtins.lessThanEqualsInteger(BigInt(5), BigInt(5)) // Or use operators val sumOp = BigInt(10) + BigInt(20) val eqOp = BigInt(42) == BigInt(42)

divideInteger uses floor division (rounds toward negative infinity), while quotientInteger uses truncated division (rounds toward zero). The difference matters for negative numbers.

ByteString Operations

Operations on immutable byte arrays, organized by category.

Constructing

import scalus.builtin.{Builtins, ByteString} // Append two ByteStrings val concatenated = Builtins.appendByteString( ByteString.fromHex("dead"), ByteString.fromHex("beef") ) // Prepend a byte (0-255) val withPrefix = Builtins.consByteString(BigInt(0xFF), ByteString.fromHex("1234"))

Inspecting

// Length of ByteString val length = Builtins.lengthOfByteString(ByteString.fromHex("deadbeef")) // 4 // Get byte at index val byte = Builtins.indexByteString(ByteString.fromHex("deadbeef"), BigInt(0)) // 0xDE // Slice ByteString val slice = Builtins.sliceByteString(BigInt(1), BigInt(2), ByteString.fromHex("deadbeef")) // hex"adbe"

Comparing

// Equality val eq = Builtins.equalsByteString( ByteString.fromHex("deadbeef"), ByteString.fromHex("deadbeef") ) // true // Lexicographic comparison val lt = Builtins.lessThanByteString( ByteString.fromHex("1234"), ByteString.fromHex("5678") ) // true val lte = Builtins.lessThanEqualsByteString( ByteString.fromHex("1234"), ByteString.fromHex("1234") ) // true

Bitwise Operations (Plutus V3+)

Perform bitwise operations on ByteStrings. The shouldPad parameter controls behavior when ByteStrings have different lengths.

See CIP-122  for specification.

// Bitwise AND val and = Builtins.andByteString(false, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"0F" val andPad = Builtins.andByteString(true, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"0FFF" // Bitwise OR val or = Builtins.orByteString(false, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"FF" // Bitwise XOR val xor = Builtins.xorByteString(false, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"F0" // Bitwise complement (NOT) val complement = Builtins.complementByteString(ByteString.fromHex("FF")) // hex"00"

Bit Manipulation (Plutus V3+)

See CIP-122  and CIP-123  for specifications.

Bit indexing uses LSB (Least Significant Bit) ordering, meaning index 0 refers to the rightmost bit. For example, in 0x04 (binary 00000100), bit 2 is set.

// Read a bit at index (index 0 is the rightmost/least significant bit) // Example: 0x0004 = binary 0000000000000100, so bit at index 2 is set val bit = Builtins.readBit(ByteString.fromHex("0004"), BigInt(2)) // true // Write bits at indices val modified = Builtins.writeBits(ByteString.fromHex("0000"), BuiltinList(BigInt(0), BigInt(1)), true) // hex"0003" // Replicate a byte n times val repeated = Builtins.replicateByte(BigInt(4), BigInt(0xFF)) // hex"FFFFFFFF"

Shifting and Rotating (Plutus V3+)

See CIP-123  for specification.

// Shift bits left (positive) or right (negative) val shiftLeft = Builtins.shiftByteString(ByteString.fromHex("000F"), BigInt(4)) // hex"00F0" val shiftRight = Builtins.shiftByteString(ByteString.fromHex("000F"), BigInt(-4)) // hex"0000" // Rotate bits val rotateLeft = Builtins.rotateByteString(ByteString.fromHex("000F"), BigInt(4)) // hex"00F0" val rotateRight = Builtins.rotateByteString(ByteString.fromHex("000F"), BigInt(-4)) // hex"F000" // Count set bits (population count) val popcount = Builtins.countSetBits(ByteString.fromHex("000F")) // 4 // Find first set bit index (-1 if none) val firstSet = Builtins.findFirstSetBit(ByteString.fromHex("0002")) // 1

Integer/ByteString Conversion (Plutus V2+)

See CIP-121  for specification.

// Convert ByteString to integer (big-endian or little-endian) val toIntBE = Builtins.byteStringToInteger(true, ByteString.fromHex("1234")) // 4660 (big-endian) val toIntLE = Builtins.byteStringToInteger(false, ByteString.fromHex("3412")) // 4660 (little-endian) // Convert integer to ByteString with specified length val fromIntBE = Builtins.integerToByteString(true, BigInt(2), BigInt(4660)) // hex"1234" val fromIntLE = Builtins.integerToByteString(false, BigInt(2), BigInt(4660)) // hex"3412" val minimal = Builtins.integerToByteString(true, BigInt(0), BigInt(4660)) // hex"1234" (minimal length)

String Operations

UTF-8 string operations.

// Concatenate strings val greeting = Builtins.appendString("Hello, ", "Cardano!") // Equality val eq = Builtins.equalsString("test", "test") // Convert to/from UTF-8 ByteString val encoded = Builtins.encodeUtf8("Hello") val decoded = Builtins.decodeUtf8(encoded)

List Operations

Operations on immutable linked lists (BuiltinList).

import scalus.builtin.{Builtins, BuiltinList} // Choose based on empty/non-empty val result = Builtins.chooseList(BuiltinList(1, 2, 3), "empty", "not empty") // "not empty" // Construct list val list = Builtins.mkCons(BigInt(0), BuiltinList(BigInt(1), BigInt(2), BigInt(3))) // Deconstruct list val head = Builtins.headList(list) // 0 val tail = Builtins.tailList(list) // [1, 2, 3] val isEmpty = Builtins.nullList(list) // false // Drop first n elements (Plutus V4+) val dropped = Builtins.dropList(BigInt(2), list) // [2, 3]

Array Operations (Plutus V4+)

O(1) indexed access to elements. See CIP-156  for specification.

Arrays are created from lists and provide efficient random access, unlike linked lists which require O(n) traversal.

import scalus.builtin.{Builtins, BuiltinList, BuiltinArray} // Convert list to array val list = BuiltinList(Builtins.iData(BigInt(10)), Builtins.iData(BigInt(20)), Builtins.iData(BigInt(30))) val array = Builtins.listToArray(list) // Get array length val len = Builtins.lengthOfArray(array) // 3 // Access element by index (O(1)) val first = Builtins.indexArray(array, BigInt(0)) // iData(10) val second = Builtins.indexArray(array, BigInt(1)) // iData(20) // Access multiple elements at once val indices = BuiltinList(BigInt(0), BigInt(2)) val selected = Builtins.multiIndexArray(indices, array) // [iData(10), iData(30)]

Pair Operations

Operations on pairs (2-tuples).

import scalus.builtin.{Builtins, BuiltinPair} val pair = BuiltinPair(BigInt(42), ByteString.fromHex("deadbeef")) // Extract elements val first = Builtins.fstPair(pair) // 42 val second = Builtins.sndPair(pair) // hex"deadbeef"

Data Operations

Operations for Plutus’s universal Data type, used for serialization and interoperability.

Creating Data

import scalus.builtin.{Builtins, Data} // Create Data from primitives val intData = Builtins.iData(BigInt(42)) val bytesData = Builtins.bData(ByteString.fromHex("deadbeef")) val listData = Builtins.listData(BuiltinList(intData, bytesData)) val mapData = Builtins.mapData(BuiltinList(Builtins.mkPairData(intData, bytesData))) val constrData = Builtins.constrData(BigInt(0), BuiltinList(intData, bytesData))

Deconstructing Data

// Extract Data values val int = Builtins.unIData(intData) val bytes = Builtins.unBData(bytesData) val list = Builtins.unListData(listData) val map = Builtins.unMapData(mapData) val (tag, fields) = Builtins.unConstrData(constrData)

Data Operations

// Choose based on Data variant val result = Builtins.chooseData( someData, constrCase = "constructor", mapCase = "map", listCase = "list", iCase = "integer", bCase = "bytestring" ) // Compare Data values val eq = Builtins.equalsData(data1, data2) // Serialize Data to CBOR (Plutus V2+) val serialized = Builtins.serialiseData(someData)

Monomorphic Constructors

// Create pairs and lists of Data val pairData = Builtins.mkPairData(intData, bytesData) val emptyList = Builtins.mkNilData() val emptyPairList = Builtins.mkNilPairData()

Cryptographic Hash Functions

Hashing

// SHA-2 and SHA-3 (Plutus V1+) val sha2 = Builtins.sha2_256(ByteString.fromString("message")) // 32 bytes val sha3 = Builtins.sha3_256(ByteString.fromString("message")) // 32 bytes // BLAKE2b (Plutus V1+) - used extensively in Cardano val blake256 = Builtins.blake2b_256(ByteString.fromString("message")) // 32 bytes val blake224 = Builtins.blake2b_224(ByteString.fromString("message")) // 28 bytes // Keccak-256 (Plutus V3+) - Ethereum compatible // Note: This is original Keccak, not NIST SHA-3 val keccak = Builtins.keccak_256(ByteString.fromString("message")) // 32 bytes // RIPEMD-160 (Plutus V4+) - Bitcoin compatible val ripemd = Builtins.ripemd_160(ByteString.fromString("message")) // 20 bytes

See CIP-158  for Keccak-256 and CIP-127  for RIPEMD-160.

Digital Signatures

// Ed25519 signature verification (Plutus V1+) // publicKey: 32 bytes, message: any length, signature: 64 bytes val validEd = Builtins.verifyEd25519Signature(publicKey, message, signature) // ECDSA SECP256k1 signature verification (Plutus V2+) // publicKey: 33 bytes (compressed), messageHash: 32 bytes (pre-hashed!), signature: 64 bytes val validEcdsa = Builtins.verifyEcdsaSecp256k1Signature(publicKey, messageHash, signature) // Schnorr SECP256k1 signature verification (Plutus V2+) // publicKey: 32 bytes (x-only), message: any length, signature: 64 bytes val validSchnorr = Builtins.verifySchnorrSecp256k1Signature(publicKey, message, signature)

For verifyEcdsaSecp256k1Signature, the message must be pre-hashed (32 bytes). For Ed25519 and Schnorr, pass the original message.

BLS12-381 Pairing Operations (Plutus V3+)

Advanced cryptographic operations for zero-knowledge proofs and pairing-based cryptography. See CIP-381  for specification.

Scalus provides convenient wrappers in scalus.prelude.crypto.bls12_381:

import scalus.prelude.crypto.bls12_381.{G1, G2, Scalar} import scalus.builtin.{BLS12_381_G1_Element, BLS12_381_G2_Element} // G1 operations (48-byte compressed points) val g1Zero = G1.zero val g1Gen = G1.generator val g1Sum = g1Point1 + g1Point2 // addition val g1Neg = -g1Point1 // negation val g1Scaled = g1Point1.scale(scalar) // scalar multiplication val g1Compressed = g1Point1.compress // to ByteString val g1FromHash = G1.hashToGroup(message, dst) // G2 operations (96-byte compressed points) val g2Zero = G2.zero val g2Gen = G2.generator val g2Sum = g2Point1 + g2Point2 val g2Neg = -g2Point1 val g2Scaled = g2Point1.scale(scalar) val g2Compressed = g2Point1.compress val g2FromHash = G2.hashToGroup(message, dst)

Low-level Builtin Functions

import scalus.builtin.Builtins // G1 Group operations val g1Sum = Builtins.bls12_381_G1_add(g1Point1, g1Point2) val g1Neg = Builtins.bls12_381_G1_neg(g1Point1) val g1Scaled = Builtins.bls12_381_G1_scalarMul(scalar, g1Point1) val g1Equal = Builtins.bls12_381_G1_equal(g1Point1, g1Point2) // G1 Serialization val g1Compressed = Builtins.bls12_381_G1_compress(g1Point) // 48 bytes val g1Uncompressed = Builtins.bls12_381_G1_uncompress(bytes) // G1 Hash to curve val g1FromHash = Builtins.bls12_381_G1_hashToGroup(message, dst) // G2 Group operations (same pattern as G1) val g2Sum = Builtins.bls12_381_G2_add(g2Point1, g2Point2) val g2Neg = Builtins.bls12_381_G2_neg(g2Point1) val g2Scaled = Builtins.bls12_381_G2_scalarMul(scalar, g2Point1) val g2Equal = Builtins.bls12_381_G2_equal(g2Point1, g2Point2) // G2 Serialization val g2Compressed = Builtins.bls12_381_G2_compress(g2Point) // 96 bytes val g2Uncompressed = Builtins.bls12_381_G2_uncompress(bytes) // G2 Hash to curve val g2FromHash = Builtins.bls12_381_G2_hashToGroup(message, dst)

Pairing Operations

Used for verifying pairing equations like e(P1, Q1) == e(P2, Q2):

// Miller loop computes intermediate pairing result val ml1 = Builtins.bls12_381_millerLoop(g1Point1, g2Point1) val ml2 = Builtins.bls12_381_millerLoop(g1Point2, g2Point2) // Multiply Miller loop results (for multi-pairing) val mlProduct = Builtins.bls12_381_mulMlResult(ml1, ml2) // Final verification: checks if e(P1,Q1) == e(P2,Q2) val isValid = Builtins.bls12_381_finalVerify(ml1, ml2)

Control Flow

// Conditional execution val result = Builtins.ifThenElse(condition, thenValue, elseValue) // Force evaluation after unit val forced = Builtins.chooseUnit()(computeValue)

Debugging

// Trace a message (collected during evaluation, doesn't affect result) val result = Builtins.trace("Debug message")(computeValue)

Trace messages are collected during script execution and can be viewed in transaction evaluation logs. They don’t affect the script result but do consume execution units.

Practical Examples

This section shows how builtin functions can be used in smart contracts.

Bit Manipulation for Compact State

Using bitwise operations for space-efficient on-chain state:

import scalus.* import scalus.builtin.Builtins.* import scalus.builtin.ByteString @Compile object BitFlags { // Store up to 256 boolean flags in a 32-byte ByteString /** Check if flag at position is set */ def isSet(flags: ByteString, position: BigInt): Boolean = readBit(flags, position) /** Set a flag at position */ def setFlag(flags: ByteString, position: BigInt): ByteString = writeBits(flags, BuiltinList(position), true) /** Clear a flag at position */ def clearFlag(flags: ByteString, position: BigInt): ByteString = writeBits(flags, BuiltinList(position), false) /** Toggle a flag at position using XOR */ def toggleFlag(flags: ByteString, position: BigInt): ByteString = { // Create a mask with only the target bit set val byteIndex = position / BigInt(8) val bitIndex = position % BigInt(8) val mask = replicateByte(byteIndex, BigInt(0)) ++ ByteString.fromArray(Array((1 << bitIndex.toInt).toByte)) ++ replicateByte(BigInt(32) - byteIndex - BigInt(1), BigInt(0)) xorByteString(false, flags, mask) } /** Count how many flags are set */ def countFlags(flags: ByteString): BigInt = countSetBits(flags) }

Usage Notes

  • All builtin functions are type-safe and validated at compile time
  • Most builtins have convenient operator syntax (e.g., +, ==, ++)
  • Builtin functions compile directly to Plutus opcodes for maximum efficiency
  • Check Plutus version compatibility when using newer builtins
  • See the Builtins API documentation  for complete details
  • Refer to the Plutus specification  for formal semantics
Last updated on