Transaction context (tx.origin vs msg.sender) in Blockchain / Solidity - Performance Comparison
When working with blockchain transactions, it is important to understand how the code checks who started or called a transaction.
We want to see how the number of steps changes when the transaction involves multiple calls.
Analyze the time complexity of the following Solidity code snippet.
contract Example {
function checkOrigin() public view returns (address) {
return tx.origin;
}
function checkSender() public view returns (address) {
return msg.sender;
}
}
This code returns the address of the original transaction sender and the immediate caller.
In this snippet, there are no loops or recursion.
- Primary operation: Accessing transaction context variables.
- How many times: Once per function call.
Since the functions only read a value, the number of steps stays the same no matter how many calls happen.
| Call Depth (n) | Approx. Operations |
|---|---|
| 1 | 1 |
| 10 | 1 |
| 100 | 1 |
Pattern observation: The work remains constant regardless of the number of calls or call depth.
Time Complexity: O(1)
This means each function call takes the same small amount of time, no matter what.
[X] Wrong: "Using tx.origin or msg.sender will slow down the contract as calls get deeper."
[OK] Correct: Accessing these variables is a simple read operation and does not depend on call depth or input size.
Understanding how transaction context variables work helps you reason about security and performance in smart contracts, a key skill for blockchain developers.
"What if the function called another contract before returning tx.origin? How would that affect the time complexity?"
Practice
tx.origin represent in a blockchain transaction?Solution
Step 1: Understand the meaning of
tx.origintx.originalways points to the external account that initiated the transaction, regardless of how many contracts are called.Step 2: Differentiate from
msg.sendermsg.senderis the immediate caller, which can be a contract or an account, buttx.originis always the original external user.Final Answer:
The original external account that started the transaction -> Option AQuick Check:
tx.origin= original external account [OK]
- Confusing tx.origin with msg.sender
- Thinking tx.origin changes in internal calls
- Assuming tx.origin is always a contract
msg.sender in Solidity?Solution
Step 1: Identify the correct variable for immediate caller
To check the caller of the function,msg.senderis used because it represents the immediate caller.Step 2: Verify the syntax of the require statement
The correct syntax isrequire(msg.sender == owner);to ensure only the owner can proceed.Final Answer:
require(msg.sender == owner); -> Option DQuick Check:
Use msg.sender for permission checks [OK]
- Using tx.origin for permission checks
- Swapping owner and caller in comparison
- Using block.coinbase which is miner address
contract Test {
address owner;
constructor() {
owner = msg.sender;
}
function checkOrigin() public view returns (bool) {
return tx.origin == owner;
}
}If an external user calls
checkOrigin() directly, what will be the returned value?Solution
Step 1: Understand owner assignment in constructor
The owner is set tomsg.senderwhen the contract is deployed, which is the external user deploying it.Step 2: Analyze
When the external user callscheckOrigin()call by external usercheckOrigin(),tx.originis the same external user who deployed the contract (owner), so the comparison returns true.Final Answer:
true -> Option BQuick Check:
Direct call: tx.origin equals owner [OK]
- Assuming tx.origin differs from msg.sender on direct calls
- Confusing owner initialization
- Expecting errors from this code
contract Caller {
address owner;
function callCheck(address callee) public view returns (bool) {
(bool success, ) = callee.call(abi.encodeWithSignature("checkOrigin()"));
return success;
}
}
contract Callee {
address owner;
constructor() {
owner = msg.sender;
}
function checkOrigin() public view returns (bool) {
return tx.origin == owner;
}
}What is the main issue with the
callCheck function in Caller?Solution
Step 1: Analyze the use of low-level call in callCheck
Thecallreturns (bool, bytes), but the code only returns the bool from call without decoding the actual function's return value.Step 2: Understand the consequence of ignoring return data
Because the function ignores the returned bytes, it cannot get the actual boolean result fromcheckOrigin(), leading to incorrect or misleading results.Final Answer:
It uses low-level call incorrectly and ignores return value unpacking -> Option CQuick Check:
Low-level call needs return data decoding [OK]
- Blaming tx.origin usage instead of call misuse
- Ignoring that call returns (bool, bytes)
- Assuming owner is set wrongly
Solution
Step 1: Understand the difference between msg.sender and tx.origin
msg.senderis the immediate caller, which can be a contract, whiletx.originis the original external account that started the transaction.Step 2: Choose the check that ensures original user authorization
To ensure only the original external user can execute the function, userequire(tx.origin == owner);because it tracks the transaction origin through multiple calls.Final Answer:
Use require(tx.origin == owner); to check the original external account -> Option AQuick Check:
Original user check uses tx.origin [OK]
- Using msg.sender which can be a contract
- Comparing tx.origin to msg.sender incorrectly
- Checking contract address instead of user
Ready to prove it?
Pass the test and this pattern moves unknown
