Leases
A lease is a mechanism in Algorand that reserves exclusive rights to submit transactions with a specific identifier for a defined period, preventing duplicate or competing transactions from the same account during that time.
Leases provide security for transactions in three ways: they enable exclusive transaction execution (useful for recurring payments), help mitigate fee variability, and secure long-running smart contracts. When a transaction includes a Lease value ([32]byte), the network creates a { Sender : Lease }
pair that persists on the validation node until the transaction’s LastValid round expires. This creates a “lock” that prevents any future transaction from using the same { Sender : Lease }
pair until expiration.
The typical one-time payment or asset “send” transaction is short-lived and may not necessarily benefit from including a Lease value, but failing to define one within certain smart contract designs may leave an account vulnerable to a denial-of-service attack.
How Leases Work
Every transaction in Algorand includes a Header with required and optional validation fields. The required fields FirstValid and LastValid define a time window of up to 1000 rounds during which the transaction can be validated by the network. On MainNet, this creates a validity window of up to 70 minutes. Smart contracts often calculate a specific validity window and include a Lease value in their validation logic to enable secure transactions for payments, key management and other scenarios.
Let’s take a look at why you may want to use the Lease field and when you definitely should.
Step by Step
Let’s examine a simple example where Alice sends Algo to Bob. This basic transaction is short-lived and typically wouldn’t need a lease under normal network conditions.
$ goal clerk send –from $ALICE –to $BOB –amount $AMOUNT
Under normal network conditions, this transaction will be confirmed in the next round. Bob gets his money from Alice and there are no further concerns.
However, now let’s assume the network is congested, fees are higher than normal and Alice desires to minimize her fee spend while ensuring only a single payment transaction to Bob is confirmed by the network. Alice may construct a series of transactions to Bob, each defining identical Lease, FirstValid and LastValid values but increasing Fee amounts, then broadcast them to the network.
# Define transaction fields$ LEASE_VALUE=$(echo "Lease value (at most 32-bytes)" | xxd -p | base64)$ FIRST_VALID=$(goal node status | grep "Last committed block:" | awk '{ print $4 }')$ VALID_ROUNDS=1000$ LAST_VALID=$(($FIRST_VALID+$VALID_ROUNDS))$ FEE=1000
# Create the initial signed transaction and write it out to a file$ goal clerk send –-from $ALICE –-to $BOB –-amount $AMOUNT \ –-lease $LEASE_VALUE --firstvalid $FIRST_VALID –-lastvalid $LAST_VALID \ –-fee $FEE –-out $FEE.stxn --sign
Above, Alice defined values to use within her transactions. The $LEASE_VALUE
must be base64 encoded and not exceed 32-bytes, typically using a hash value. The $FIRST_VALID
value is obtained from the network and $VALID_ROUNDS
is set to its maximum value of 1000 to calculate $LAST_VALID
. Initially $FEE
is set to the minimum and will be the only value modified in subsequent transactions.
Alice now broadcasts the initial transaction with goal clerk rawsend –-filename 1000.stxn
but due to network congestion and high fees, goal
will continue awaiting confirmation until $LAST_VALID
. During the validation window Alice may construct additional nearly identical transactions with only higher fees and broadcast each one concurrently.
# Redefine ONLY the FEE value$ FEE=$(($FEE+1000))
# Broadcast additional signed transaction$ goal clerk send –-from $ALICE –-to $BOB –-amount $AMOUNT \ –-lease $LEASE_VALUE --firstvalid $FIRST_VALID –-lastvalid $LAST_VALID \ –-fee $FEE
Alice will continue to increase the $FEE
value with each subsequent transaction. At some point, one of the transactions will be approved, likely the one with the highest fee at that time, and the “lock” is now set for { $ALICE : $LEASE_VALUE }
until $LAST_VALID
. Alice is assured that none of her previously submitted pending transaction can be validated. Bob is paid just one time.
Potential Pitfalls
That was a rather simple scenario and unlikely during normal network conditions. Next, let’s uncover some security concerns Alice needs to guard against. Once Alice broadcasts her initial transaction, she must ensure all subsequent transactions utilize the exact same values for FirstValid, LastValid and Lease. Notice in the second transaction only the Fee is incremented, ensuring the other values remain static. If Alice executes the initial code block twice, the $FIRST_VALID
value will be updated by querying the network presently, thus extending the validation window for $LEASE_VALUE
to be evaluated.
Similarly, if the $LEASE_VALUE
is changed within a static validation window, multiple transactions may be confirmed. Remember, the “lock” is a mutual exclusion on { Sender : Lease }
; changing either creates a new lock.
After the validation window expires, Alice is free to reuse the $LEASE_VALUE
in any new transaction. This is a common practice for recurring payments.
Code implementation
Following you will find an example of implementing leases using Algokit Utils in Python and Typescript
// Create a lease value - this could be any unique string or Uint8Array const lease: string | Uint8Array = 'unique-lease-value'
// Send a payment transaction with a lease // If another transaction with the same lease and sender tries to execute within the validity window, // it will be rejected await algorand.send.payment({ sender: randomAccountA, receiver: randomAccountB, amount: algo(1), lease: lease, // Optional: Set a custom validity window for the lease validityWindow: 100, // Number of rounds the lease is valid for })
// Attempting to send another transaction with the same lease and sender within the validity window // will cause the transaction to be rejected try { await algorand.send.payment({ sender: randomAccountA, // Same sender as first transaction receiver: randomAccountC, amount: algo(10), lease: lease, suppressLog: true, // Prevent AlgoKit Utils from logging the expected error }) } catch (_error) { console.log('Transaction rejected due to active lease') }
# Create a lease value - this could be any unique string or Uint8Array lease: bytes = b"unique-lease-value"
""" Send a payment transaction with a lease If another transaction with the same lease and sender tries to execute within the validity window, it will be rejected """ result = algorand_client.send.payment( PaymentParams( sender=account_a.address, receiver=account_a.address, amount=AlgoAmount(algo=1), lease=lease, # Optional: Set a custom validity window for the lease validity_window=2, # Number of rounds the lease is valid for ) )
""" Attempting to send another transaction with the same lease and sender within the validity window will cause the transaction to be rejected """ try: algorand_client.send.payment( PaymentParams( sender=account_a.address, # Same sender as first transaction receiver=account_a.address, amount=AlgoAmount(algo=1), lease=lease, ), send_params=SendParams( suppress_log=True, ), ) except Exception as e: print("Transaction rejected due to active lease")
""" Sending empty payment txn to progress the block round """ algorand_client.send.payment( PaymentParams( sender=account_a.address, receiver=account_a.address, amount=AlgoAmount(algo=0), ) )
""" This payment transaction with the same lease and sender will be accepted because the first_valid_round is outside the validity window of the first transaction """ pay_txn = algorand_client.send.payment( PaymentParams( sender=account_a.address, # Same sender as first transaction receiver=account_a.address, amount=AlgoAmount(algo=1), lease=lease, ) )