Learn how to develop and build programs for the Thru network using the C SDK
Thru programs are executable code that runs on the Thru blockchain VM, handling transactions and managing account state. This guide walks you through building a complete counter program using the C SDK.
While this guide uses C, Thru supports multiple programming languages through different SDKs. The core concepts remain the same across all supported languages.
Before diving into program structure, it’s important to understand the fundamental concepts of Thru program development.Entry Point Function
Every Thru program must define an entry point function with the TSDK_ENTRYPOINT_FN attribute. This function serves as the main execution entry point for your program.
Copy
Ask AI
TSDK_ENTRYPOINT_FN void start(uchar const *instruction_data, ulong instruction_data_sz) { // Your program logic here}
The entry point receives:
instruction_data: Raw instruction data passed to your Thru program
instruction_data_sz: Size of the instruction data in bytes
Program Execution Model
Thru programs don’t return values in the traditional sense. Instead, they either complete successfully using tsdk_return() or terminate with an error using tsdk_revert(). The program execution simply exits when complete.
Use tsdk_return() for successful completion:
Copy
Ask AI
tsdk_return(TSDK_SUCCESS);
Use tsdk_revert() to terminate with an error:
Copy
Ask AI
tsdk_revert(error_code);
Input Validation
Always validate input data size and format to prevent security vulnerabilities and ensure program reliability. This is critical for preventing buffer overflows and malformed data attacks.
We’ll cover detailed validation patterns when we implement the entry point function.SDK ImportsWhen developing programs using the SDK, include these essential headers:
<thru-sdk/c/tn_sdk.h> - Core SDK functions and macros
<thru-sdk/c/tn_sdk_syscall.h> - System call functions (optional, for advanced operations)
# Simple makefile for my Thru projectBASEDIR:=$(CURDIR)/build# Set THRU_C_SDK_DIR to the location Thru SDK install. The default directory# is already set.THRU_C_SDK_DIR:=$(HOME)/.thru/sdk/c/thru-sdkinclude $(THRU_C_SDK_DIR)/thru_c_program.mk
The GNUmakefile sets up paths to the installed Thru C SDK and includes the program build rules.
3
Create Program Build Configuration
Create a Local.mk file in your examples directory:
examples/Local.mk
Copy
Ask AI
# My Thru C SDK Counter Program$(call make-bin,tn_counter_program_c,tn_counter_program,,-ltn_sdk)
The Local.mk file tells the build system which programs to compile using the make-bin macro, linking against the tn_sdk library.The names tn_counter_program_c (output binary name) and tn_counter_program (source file name) will be used for the files we create later in this guide.
Create examples/tn_counter_program.h with the following content. This header defines the data structures, error codes, and constants for your counter program:
Create examples/tn_counter_program.c with the following content. This file contains the program logic including instruction handlers and the entry point:
tn_counter_program.c
Copy
Ask AI
#include <stddef.h>#include <thru-sdk/c/tn_sdk.h>#include <thru-sdk/c/tn_sdk_syscall.h>#include "tn_counter_program.h"static void handle_create_counter(uchar const *instruction_data, ulong instruction_data_sz TSDK_PARAM_UNUSED) { tn_counter_create_args_t const *args = (tn_counter_create_args_t const *)instruction_data; /* Use account index from instruction arguments (index 0 is the fee payer, index 1 is the program) */ ushort account_idx = args->account_index; /* Get proof data pointer (follows the struct) */ uchar const *proof_data = NULL; if (args->proof_size > 0) { proof_data = instruction_data + sizeof(tn_counter_create_args_t); } /* Create the account using seed and proof from instruction data */ ulong result = tsys_account_create(account_idx, args->counter_program_seed, proof_data, args->proof_size); if (result != TSDK_SUCCESS) { tsdk_revert(TN_COUNTER_ERR_ACCOUNT_CREATE_FAILED); } /* Set account as writable so we can modify its data */ result = tsys_set_account_data_writable(account_idx); if (result != TSDK_SUCCESS) { tsdk_revert(TN_COUNTER_ERR_ACCOUNT_SET_WRITABLE_FAILED); } /* Resize account to hold counter data */ result = tsys_account_resize(account_idx, sizeof(tn_counter_account_t)); if (result != TSDK_SUCCESS) { tsdk_revert(TN_COUNTER_ERR_ACCOUNT_RESIZE_FAILED); } /* Get account data pointer and initialize counter */ void* account_data = tsdk_get_account_data_ptr(account_idx); if (account_data == NULL) { tsdk_revert(TN_COUNTER_ERR_ACCOUNT_DATA_ACCESS_FAILED); } tn_counter_account_t* counter_account = (tn_counter_account_t*)account_data; counter_account->counter_value = 0UL; tsdk_return(TSDK_SUCCESS);}static void handle_increment_counter(uchar const *instruction_data, ulong instruction_data_sz TSDK_PARAM_UNUSED) { tn_counter_increment_args_t const *args = (tn_counter_increment_args_t const *)instruction_data; ushort account_idx = args->account_index; /* Get account data pointer */ void* account_data = tsdk_get_account_data_ptr(account_idx); if (account_data == NULL) { tsdk_revert(TN_COUNTER_ERR_ACCOUNT_DATA_ACCESS_FAILED); } /* Set account as writable so we can modify the counter value */ ulong result = tsys_set_account_data_writable(account_idx); if (result != TSDK_SUCCESS) { tsdk_revert(TN_COUNTER_ERR_ACCOUNT_SET_WRITABLE_FAILED); } /* Increment the counter */ tn_counter_account_t* counter_account = (tn_counter_account_t*)account_data; counter_account->counter_value++; /* Emit increment event - emit just the counter value */ tsys_emit_event((uchar const *)&counter_account->counter_value, sizeof(ulong)); tsdk_return(TSDK_SUCCESS);}TSDK_ENTRYPOINT_FN void start(uchar const *instruction_data, ulong instruction_data_sz) { /* Check minimum size to safely read instruction type */ if (instruction_data_sz < sizeof(uint)) { tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE); } uint const *instruction_type = (uint const *)instruction_data; switch (*instruction_type) { case TN_COUNTER_INSTRUCTION_CREATE: /* Check minimum size to safely access struct fields */ if (instruction_data_sz < sizeof(tn_counter_create_args_t)) { tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE); } tn_counter_create_args_t const *create_args = (tn_counter_create_args_t const *)instruction_data; ulong expected_size = sizeof(tn_counter_create_args_t) + create_args->proof_size; /* Check exact size including proof data */ if (instruction_data_sz != expected_size) { tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE); } handle_create_counter(instruction_data, instruction_data_sz); break; case TN_COUNTER_INSTRUCTION_INCREMENT: /* Check exact instruction size */ if (instruction_data_sz != sizeof(tn_counter_increment_args_t)) { tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE); } handle_increment_counter(instruction_data, instruction_data_sz); break; default: tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_TYPE); } /* Should never reach here */ tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_TYPE);}
Key components:
Handler functions: Separate functions for create and increment operations
Entry point: The start function validates input and routes to the appropriate handler
Input validation: Comprehensive size checks before accessing memory
Error handling: Uses tsdk_revert() to exit with error codes
With your program files in place, build the program:
1
Build the Program
Run the build from your project root:
Copy
Ask AI
make
The build system compiles your Thru program for the Thru VM using the installed RISC-V toolchain, generating a .bin file in the build/thruvm/bin/ directory ready for deployment.
Your program is successfully compiled and ready for deployment to the Thru network.
Great! Your counter program is now compiled and ready. The next step is deploying it to the Thru network, which makes your program available for other users and applications to interact with.Deploy your program to the Thru network using the CLI:
1
Create Program on Network
Upload and create your program using a unique seed name:
Copy
Ask AI
thru-cli program create <SEED> <PATH_TO_PROGRAM_BINARY>
For our counter example:
Copy
Ask AI
thru-cli program create thru_program ./build/thruvm/bin/tn_counter_program_c.bin
You’ll see output similar to this:
Copy
Ask AI
Info: Creating permanent managed program from file: ./build/thruvm/bin/tn_counter_program_c.bin (778 bytes)Info: User seed: thru_programInfo: Step 1: Uploading program to temporary buffer (seed: thru_program_temporary)Info: 🔍 Checking for existing upload state...Info: ℹ No existing upload foundInfo: 🚀 Starting fresh uploadInfo: Creating meta and buffer accounts...Success: Transaction completed: tsBBvaXmtyKDn95pAeo03DEiHlobsAc5t80NoFKqFzEwd_5q2PoGTdXNl9gvNU_PmFsSWy168mFcmRMmhL2VsICB8hInfo: Writing chunk 1/1 (778 bytes) at offset 0Success: Transaction completed: ts5ubIqevoNhioFKswmUolG-7-umnXZwNhTFLD00XQFu05xhspnZK4A6JpgwHr80kW4-fMHHbG44skZzCzZSb1CCBgInfo: Finalizing upload...Success: Transaction completed: tsXzyg8MIISD4keoNHaTsxEruagay7Q9vcDs69FyDKpjOgiUgrpr5IZLdfesX9n-si-CoFn0bLMJara156AmsADR0jSuccess: ✓ Program uploaded to temporary buffer successfullyInfo: Temporary meta account: taOxQq4ms4bF2lxs4gM1tkZS90q2ACVo9xvuw-UTzAOO3XInfo: Temporary buffer account: taqrAKYvM6KxZMxotJzaEUhkNQxum-AGr4OKrgN6uCvMMpInfo: Step 2: Creating managed program from temporary bufferInfo: Fee payer: tazLZyk2wT3WO1-_vgH2iqNi0nayD5z2jfFcK10hgrSpanInfo: Manager program meta account: ta8GX2vn4xeY-hGHgrnARdDhz9q3EtcKMnQYOCAtMOza9oInfo: Manager program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4Info: Creating state proofs for permanent program...Success: Transaction completed: tsq6iZBJwRmShKrVZXl0TrzdVam6LCQ1GdyXXpMuOmfvfpq9BhhHkqwx1jwj3ud4l1V4hxxxoZR1fd94z2UTlYCSEFSuccess: ✓ Managed program created successfullyInfo: Step 3: Cleaning up temporary buffer accountInfo: Cleaning up program accounts...Info: Executing cleanup transaction...Success: Transaction completed: ts12OV8Y2p6c-UVaqctw2exA2GOdro7CuXSr5fqLEKMlEWrrilxj-1YuaGsRbtdMpyln5wMuy456OcLjvZRrxLDCK0Success: ✓ Temporary buffer account cleaned up successfullySuccess: 🎉 Permanent managed program created successfully!Info: Meta account: ta8GX2vn4xeY-hGHgrnARdDhz9q3EtcKMnQYOCAtMOza9oInfo: Program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
2
Note Your Program Accounts
Save the program account addresses from the output:
Program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
When you deploy a program, two accounts are created:
Meta account - Stores management metadata (authority, version counter, pause state)
Program account - Contains the actual executable program code
The two accounts are cryptographically linked: the meta account is derived from your seed, and the program account is derived from the meta account’s address. You’ll use the program account address when calling your program from client applications.
Your counter program is now deployed and ready to receive create and increment instructions on the Thru network.
When you need to modify and redeploy your program:
1
Make Code Changes and Rebuild
After making changes to your program code, rebuild it:
Copy
Ask AI
make
Always rebuild your program after making code changes to ensure the binary file contains your latest updates.
2
Upgrade the Deployed Program
Use the same seed to upgrade your existing program:
Copy
Ask AI
thru-cli program upgrade <SEED> <PATH_TO_PROGRAM_BINARY>
For our counter example:
Copy
Ask AI
thru-cli program upgrade thru_program ./build/thruvm/bin/tn_counter_program_c.bin
You’ll see output similar to this:
Copy
Ask AI
Info: Upgrading managed program from file: ./build/thruvm/bin/tn_counter_program_c.bin (778 bytes)Info: User seed: thru_programInfo: Step 1: Uploading program to temporary buffer (seed: thru_program_upgrade_temporary)Info: 🔍 Checking for existing upload state...Info: ℹ No existing upload foundInfo: 🚀 Starting fresh uploadInfo: Creating meta and buffer accounts...Success: Transaction completed: tsVAIY50dLC3vjkBC0y08yeoXcoa3M6Mzb9jG6pscofprvMs6SEvYUNOZuXJb0Xx1_1kPh_wVMpRMnlukmvgduCiATInfo: Writing chunk 1/1 (778 bytes) at offset 0Success: Transaction completed: tsN2k0h3HS4G8LW_LYYzAumXLYVOfRA4cXiQHFVVjb__kXKjxAM4BF_dMQasIvXKHT8PlGE673B3nS_os_VnObCR9rInfo: Finalizing upload...Success: Transaction completed: tso9ORuCIqpr2hZBgtBPC1aKZrFHP8mdbWrCr849R4LcSOLcQmJyR7foDbsgoM6lsrCK8jM_5JtQ5DOrw-t0_CCh5wSuccess: ✓ Program uploaded to temporary buffer successfullyInfo: Temporary meta account: taiMvWgn2SAj5ZHmMCYaja5VzOu-8MmZBTujAVDLdcXYL3Info: Temporary buffer account: taLsLf6_wdbPXMklS2Ss3VqJNCHnqRSy1CXzb0ZdNwtTH_Info: Step 2: Upgrading managed program from temporary bufferInfo: Fee payer: tazLZyk2wT3WO1-_vgH2iqNi0nayD5z2jfFcK10hgrSpanInfo: Manager program meta account: ta8GX2vn4xeY-hGHgrnARdDhz9q3EtcKMnQYOCAtMOza9oInfo: Manager program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4Success: Transaction completed: tse-0ldYMRrs45XaDbDTfSnFIXtuDQdY_pd5kDUyrf0iJkZE1u-l8KQntTgWlNdx-QsAPInqINLfjru7UtDHMQBR1QSuccess: ✓ Managed program upgraded successfullyInfo: Step 3: Cleaning up temporary buffer accountInfo: Cleaning up program accounts...Info: Executing cleanup transaction...Success: Transaction completed: tsfQJTncHpt-_lFf0aH6Wefparw7_59NQEv6dQ3kDLJuLNW9-_6nOC0El1NOgNr7854zGE9yCM4Kw7hbTwvC-OASSHSuccess: ✓ Temporary buffer account cleaned up successfullySuccess: Program upgrade completed! New program size: 778 bytes
The upgrade process uses the same program accounts but updates the program code. Your program account addresses remain the same, so existing client applications continue to work without changes.
Perfect! Your counter program is now deployed and ready to use. But before we can send transactions to it, we need to understand how to properly format instructions and organize accounts.
When your program executes, it receives an account array with accounts organized in a specific order. Understanding this structure is essential for properly constructing transactions and accessing the right accounts in your program.Account Array Structure:
The transaction system organizes accounts in the following order:
Fee Payer Account - The account paying transaction fees (always at index 0)
Program Account - Your deployed program (always at index 1)
Writable Accounts - Accounts the program can modify (sorted in ascending order by hex key)
Read-only Accounts - Accounts the program can only read (sorted in ascending order by hex key)
CLI Transaction Parameters:
When using thru-cli txn execute <PROGRAM> <INSTRUCTIONS>:
<PROGRAM> - Program account address (becomes index 1 in the account array)
<INSTRUCTIONS> - Hex-encoded instruction data (contains account_index specifying which account slot to use)
How Account Creation Works:
When you call create counter with a seed, the program uses tsys_account_create() to create a new account. The seed determines the account address - the same seed will always generate the same account address. This means you can:
Use a unique seed to create a counter account
Calculate the resulting account address from the seed
Use that account address in increment instructions
The account address is deterministically derived from the seed, so you don’t need the program to “return” it - you can calculate it independently.
Once your counter program is deployed, you can send transactions to interact with it. This involves constructing the proper instruction data and sending it to your program account.
To interact with your counter program, you need to understand how to format the instruction data. Let’s examine the instruction structures defined in your program:
The create instruction is more complex because it includes a variable-length proof that follows the fixed-size struct. The proof data is not part of the struct definition but follows it dynamically based on the proof_size field.
Success: State proof created successfullyAccount: tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1RProof Type: creatingProof Size: 104 bytesProof Data (hex): 3502000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
State proofs verify that an account exists or doesn’t exist in the blockchain state tree. For account creation, we use a “creating” proof.
3
Construct Create Counter Instruction Data
Now construct the instruction data by encoding each field according to the struct definition. Recall our create instruction structure:
The __attribute__((packed)) attribute is crucial - it tells the compiler to remove padding between struct members, allowing us to concatenate the hex values directly without considering alignment bytes.
For Production Use: While we show manual hex construction here for educational purposes, you’d typically use a client library or SDK to handle this encoding automatically. This detailed breakdown helps you understand what happens under the hood.
1. Instruction Type (CREATE = 0): uint in little endian
Copy
Ask AI
Value: 0 → 00000000
2. Account Index (2): ushort in little endian
Copy
Ask AI
Value: 2 → 0200
Account index 2 refers to the first writable account in the transaction (index 0 = fee payer, index 1 = program, index 2+ = other accounts).
3. Counter Program Seed: 32-byte array, zero-padded
Copy
Ask AI
Seed: "count_acc" (9 bytes)Hex: 636f756e745f616363Padded to 32 bytes: 636f756e745f6163630000000000000000000000000000000000000000000000
4. Proof Size (104 bytes): uint in little endian
Copy
Ask AI
Value: 104 → 68000000
The proof_size field indicates the size of the cryptographic proof needed to verify the account in the state tree. If proof_size = 0, no proof is needed.
Important: The --readwrite-accounts parameter uses tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R - this is the program-derived address we computed earlier using the seed “count_acc”. This is the address where our counter account will be created and stored.
thru-cli txn execute - Base command for transaction execution
--fee 0 - Set transaction fee to 0 for testing
--readwrite-accounts <account> - The program-derived address where the counter will be created/stored
<program_address> - Your deployed program’s account address
<instruction_data> - The hex-encoded instruction data we constructed
The account ordering follows the transaction specification: fee payer (index 0), program (index 1), then writable accounts (index 2+). Our counter account will be at index 2. The --readwrite-accounts parameter tells the transaction system which accounts the program is allowed to modify - in this case, the counter account we’re about to create. Note that State Units Consumed: 1 confirms the account was successfully created.
2
Execute Increment Counter Transaction
After successfully creating a counter, you can increment it:
Reminder: We use the same --readwrite-accounts tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R because we’re modifying the same counter account that was created in the previous step.
The increment instruction is much shorter since it only contains the instruction type (1) and account index (2).Event Explanation: The event shown above is emitted by our program when the counter is successfully incremented. Notice how the counter value went from 0 (initial value after creation) to 1 (after increment). The event data 0100000000000000 represents our new counter value 1 in little endian 64-bit format - this is the value we emitted using tsys_emit_event() in our program code.
🎉 Congratulations! You have successfully:
Built a complete Thru program in C
Deployed it to the Thru network
Created a counter account with state proof
Incremented the counter and verified the event emission
Your counter program demonstrates all the essential concepts: account creation, state management, input validation, and event emission.
Extend the counter: Add decrement, reset, or set value operations
Add access control: Restrict who can modify specific counters
Build more complex programs: Try programs that interact with multiple accounts
Explore the SDK: Check out advanced syscalls for more complex operations
The patterns you’ve learned here - program structure, input validation, account management, and event emission - form the foundation for any Thru program, regardless of complexity.