Skip to content

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')
}