A practical guide for understanding Soroban’srequire_auth
andrequire_auth_for_args
. Code provided. Bring a friend.
It’s no secret that getting authentication right for smart contracts is both critically important and dizzyingly difficult. There are many steps and layers and any missed crack or crevice in your implementation could leave you open to catastrophic vulnerabilities.
Much works has been done to make smart contract auth more fool proof and this is nowhere more evident than in the approach Soroban has taken in its opinionated and well researched implementation.
I’m talking about Soroban’s require_auth
and require_auth_for_args
methods on the Address
types. It’s a simple concept really once you understand it and it allows for highly composable yet bullet proof authentication opportunities for your contract designs.
Let’s begin with the example repo:
And the docs:
Our time will be spent on two distinct areas, the contract and the client. Any good smart contract developer worth their salt will spend significant time considering both of these. Let’s start on the contract side inside the ./contracts/authplore_1/src/lib.rs
file.
#![no_std]
use soroban_sdk::{contract, contractimpl, Address, Env};
#[contract]
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn run(_env: Env, source: Address) -> bool {
source.require_auth();
true
}
}
Could not be any simpler. In this contract all we’re testing is source.require_auth()
where the idea is we need to ensure that the source
Address
has signed for any given run
invocation. But how can we test and observe this? The client! Let’s take a look at ./index_1.ts
index_1.ts
There’s a bit of fun going on in that file but before we get far into it let’s ensure we’re setup to actually run it. For that we need to start a local network. We do that with the little ./docker.sh
bash script
./docker.sh
Starting Stellar Quickstart
versions:
quickstart: 307497674a7ce50780a22f53918769dbbf4e09eb
stellar-core:
v20.3.0
rust version: rustc 1.74.1 (a28077b28 2023-12-04)
soroban-env-host:
curr:
package version: 20.2.0
git version: 1bfc0f2a2ee134efc1e1b0d5270281d0cba61c2e
ledger protocol version: 20
pre-release version: 0
rs-stellar-xdr:
package version: 20.1.0
git version: 8b9d623ef40423a8462442b86997155f2c04d3a1
base XDR git version: b96148cd4acc372cc9af17b909ffe4b12c43ecb6
horizon:
horizon-v2.28.3-(built-from-source)
go1.22.1
soroban-rpc:
soroban-rpc 20.3.0 (a59f5f421a27bab71472041fc619dd8b0d1cf902) HEAD
stellar-xdr b96148cd4acc372cc9af17b909ffe4b12c43ecb6
mode: ephemeral
network: local
network passphrase: Standalone Network ; February 2017
network id: baefd734b8d3e48472cff83912375fedbc7573701912fe308af730180f97d74a
network root secret key: SC5O7VZUXDJ6JBDSZ74DSERXL7W3Y5LTOAMRF7RQRL3TAGAPS7LUVG3L
network root account id: GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI
postgres user: stellar
postgres password: 2hQPU59NAysZUoI3
finalize-pgpass: ok
...
...etc...
...
From here we need to deploy our authplore contracts to this local network so we can invoke them. (if you don’t yet have Bun installed you can do that following the instructions here)
bun install
bun run deploy.ts
[0.15ms] ".env.local"
bun install v1.0.33 (9e91e137)
Checked 74 installs across 69 packages (no changes) [40.00ms]
cleaned target
created account
cargo rustc --manifest-path=contracts/authplore_1/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release
Compiling num-traits v0.2.17
Compiling escape-bytes v0.1.1
Compiling ethnum v1.5.0
Compiling static_assertions v1.1.0
Compiling stellar-xdr v20.1.0
Compiling soroban-env-common v20.3.0
Compiling soroban-env-guest v20.3.0
Compiling soroban-sdk v20.5.0
Compiling soroban-authplore-1 v0.0.0 (/Users/tylervanderhoeven/Desktop/Web/Soroban/soroban-authplore/contracts/authplore_1)
Finished release [optimized] target(s) in 3.64s
cargo rustc --manifest-path=contracts/authplore_2/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release
Compiling soroban-authplore-2 v0.0.0 (/Users/tylervanderhoeven/Desktop/Web/Soroban/soroban-authplore/contracts/authplore_2)
Finished release [optimized] target(s) in 0.20s
cargo rustc --manifest-path=contracts/authplore_3/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release
Compiling soroban-authplore-3 v0.0.0 (/Users/tylervanderhoeven/Desktop/Web/Soroban/soroban-authplore/contracts/authplore_3)
Finished release [optimized] target(s) in 0.17s
cargo rustc --manifest-path=contracts/authplore_4/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release
Compiling soroban-authplore-4 v0.0.0 (/Users/tylervanderhoeven/Desktop/Web/Soroban/soroban-authplore/contracts/authplore_4)
Finished release [optimized] target(s) in 0.18s
built contracts
deployed contracts
✅
Assuming the above all passes we’re ready to dig into the index_1.ts
file.
import { Account, Keypair, Networks, Operation, SorobanRpc, TransactionBuilder, nativeToScVal, scValToNative } from "@stellar/stellar-sdk";
import { $ } from "bun";
if (
!Bun.env.CONTRACT_ID_1
|| !Bun.env.SECRET
) throw new Error('Missing .env.local file. Run `bun run deploy.ts` to create it.')
const horizonUrl = 'http://localhost:8000'
const rpcUrl = `${horizonUrl}/soroban/rpc`
const rpc = new SorobanRpc.Server(rpcUrl, { allowHttp: true })
const keypair = Keypair.fromSecret(Bun.env.SECRET)
const pubkey = keypair.publicKey()
const contractId = Bun.env.CONTRACT_ID_1
const networkPassphrase = Networks.STANDALONE
const source = await rpc
.getAccount(pubkey)
.then((account) => new Account(account.accountId(), account.sequenceNumber()))
.catch(() => { throw new Error(`Issue with ${pubkey} account. Ensure you're running the \`./docker.sh\` network and have run \`bun run deploy.ts\` recently.`) })
This is all just setup code getting our little app ready to make calls to the local network we just booted up. You’ll notice that deploy.ts
file saved some helpful variables to the .env.local
file which we’ll make use of throughout our app.
const simTx = new TransactionBuilder(source, {
fee: '100',
networkPassphrase
})
.addOperation(Operation.invokeContractFunction({
contract: contractId,
function: 'run',
args: [
nativeToScVal(pubkey, { type: 'address' })
]
}))
.setTimeout(0)
.build()
let tx
let simRes = await rpc.simulateTransaction(simTx)
From here we run a basic simulation against our contractId
for the run
contract with a singular pubkey
argument. This pubkey
will thus be the source
Address
we’re attempting to require_auth
for. So how will this work?
if (SorobanRpc.Api.isSimulationSuccess(simRes)) {
simRes.result?.auth.forEach(async (entry) => {
const authEntry = await $`echo ${entry.toXDR('base64')} | soroban lab xdr dec --type SorobanAuthorizationEntry --output json`.json()
console.log(JSON.stringify(authEntry, null, 2))
})
tx = SorobanRpc.assembleTransaction(simTx, simRes).build()
} else {
console.log(await rpc._simulateTransaction(simTx));
throw new Error('Failed to simulate')
}
If the simulation is successful part of the response will be a simRes.result?.auth
response which, if it exists, will contain an array of auth entries which we can inspect, or in our case, sign for final return back to the network during transaction submission when signatures are actually checked.
So how will this work? How do we get these auth entries signed?
tx.sign(keypair)
const sendRes = await rpc.sendTransaction(tx)
if (sendRes.status === 'PENDING') {
await Bun.sleep(5000);
const getRes = await rpc.getTransaction(sendRes.hash)
if (getRes.status !== 'NOT_FOUND') {
console.log(
getRes.status,
scValToNative(simRes.result!.retval)
)
} else console.log(await rpc._getTransaction(sendRes.hash))
} else console.log(await rpc._sendTransaction(tx))
We just sign the tx
. That’s it! Magic! Let’s go ahead and run this all right now.
bun run index_1.ts
{
"credentials": "source_account",
"root_invocation": {
"function": {
"contract_fn": {
"contract_address": "CDE33TNEEPZFUDKHUCF5DPGWODOV6GPEHXSSEZAC46ASAYXGQWXEXUM5",
"function_name": "run",
"args": [
{
"address": "GCQSMJBLOODUTKXH4FJYRWAGUMESVUYZGMJY76FFJSNJLLIJWH43ZTCM"
}
]
}
},
"sub_invocations": []
}
}
SUCCESS true
SUCCESS true
. A thing of beauty. But how? See that "credentials": "source_account",
line? That is telling Stellar to check the signature of the args data against the source account for the whole transaction. So in our case the contract source
arg is GCQSM...3ZTCM
and the source of the transaction is also GCQSM...3ZTCM
. Why? Because the source
of the txn was pubkey
const source = await rpc
.getAccount(pubkey)
as well as the args
of the contract invocation
args: [
nativeToScVal(pubkey, { type: 'address' })
]
This is an awesome little shortcut for signing authentication entries, the inheritance of the transaction signature down into the contract auth entries. In many cases your work will end here. The account submitting the transaction is the same account that’s signing for internal contract require_auth
requests. However what if these accounts are not the same? Or what if you need to authenticate more than one Address
?
Good question, let’s explore in ./index_2.ts
which only has a few key differences which I’ll highlight below:
... // Difference 1
const signer = Keypair.random()
const signerPubkey = signer.publicKey()
await horizon.friendbot(signerPubkey).call()
... // Difference 2
args: [
nativeToScVal(signerPubkey, { type: 'address' })
]
... // Difference 3
const { sequence } = await rpc.getLatestLedger()
for (const op of authTx.operations) {
const auths = (op as Operation.InvokeHostFunction).auth
if (!auths?.length)
continue;
for (let i = 0; i < auths.length; i++) {
auths[i] = await authorizeEntry(
auths[i],
signer,
sequence + 12,
networkPassphrase
)
}
}
simRes = await rpc.simulateTransaction(authTx)
if (SorobanRpc.Api.isSimulationSuccess(simRes)) {
simRes.result?.auth.forEach(async (entry) => {
const authEntry = await $`echo ${entry.toXDR('base64')} | soroban lab xdr dec --type SorobanAuthorizationEntry --output json`.json()
console.log('SIGNED', JSON.stringify(authEntry, null, 2))
})
tx = SorobanRpc.assembleTransaction(authTx, simRes).build()
} else {
console.log(await rpc._simulateTransaction(authTx));
throw new Error('Failed to resimulate')
}
There are 3 key differences to note here from the previous file. First is we’re generating and funding a random account which will become our contract invocation account. Second we’re using this account as the arg of the invocation which will now require that account to be the source
of the require_auth
call vs what we had prior with the txn source. Finally we add the necessary code to handle Soroban authentication cases by signing individual auth entries and then re-simulating the transaction before the final transaction signing and submission.
Take special note of this block which is where the “magic” of manual auth entry signing happens.
auths[i] = await authorizeEntry(
auths[i],
signer,
sequence + 12,
networkPassphrase
)
This authorizeEntry
method takes in the auth entry, the signing key, a duration argument for the length the signature should be considered valid and the network passphrase for the signature.
Let’s run this code and take a look at what get’s logged.
SIGN ME {
"credentials": {
"address": {
"address": "GDZKMWNH2M3WBHOFKQ76AOTMPB6I5DQR3LM5DEZP5OJBQPVQSVKU3SKZ",
"nonce": 1516956514802343400,
"signature_expiration_ledger": 0,
"signature": "void"
}
},
"root_invocation": {
"function": {
"contract_fn": {
"contract_address": "CDE33TNEEPZFUDKHUCF5DPGWODOV6GPEHXSSEZAC46ASAYXGQWXEXUM5",
"function_name": "run",
"args": [
{
"address": "GDZKMWNH2M3WBHOFKQ76AOTMPB6I5DQR3LM5DEZP5OJBQPVQSVKU3SKZ"
}
]
}
},
"sub_invocations": []
}
}
SIGNED {
"credentials": {
"address": {
"address": "GDZKMWNH2M3WBHOFKQ76AOTMPB6I5DQR3LM5DEZP5OJBQPVQSVKU3SKZ",
"nonce": 1516956514802343400,
"signature_expiration_ledger": 4258,
"signature": {
"vec": [
{
"map": [
{
"key": {
"symbol": "public_key"
},
"val": {
"bytes": "f2a659a7d337609dc5543fe03a6c787c8e8e11dad9d1932feb92183eb095554d"
}
},
{
"key": {
"symbol": "signature"
},
"val": {
"bytes": "934e7b2f6d657b7eb9f0e6faf9c38797f735aff105bd9cfcad386976b286fde49d9b36b5e7ebb63c9b6a4571613c131b72603bc6b21508709c6efa569e79aa03"
}
}
]
}
]
}
}
},
"root_invocation": {
"function": {
"contract_fn": {
"contract_address": "CDE33TNEEPZFUDKHUCF5DPGWODOV6GPEHXSSEZAC46ASAYXGQWXEXUM5",
"function_name": "run",
"args": [
{
"address": "GDZKMWNH2M3WBHOFKQ76AOTMPB6I5DQR3LM5DEZP5OJBQPVQSVKU3SKZ"
}
]
}
},
"sub_invocations": []
}
}
SUCCESS true
SUCCESS true
. Look at us being all successful! Observe then the two "credentials":
objects. The first is that initial simulation which clues our client into the fact that a slightly fancier signing scheme is required this time vs the simple "source_account"
from before. We need an address
, nonce
, signature_expiration_ledger
and most importantly the actual signature
. The root_invocation
contains the data we’ll be signing, which we then do via that authorizeEntry
function. Once we have our transaction updated with the signed auth entries we can re-simulate to get the new fully assembled and resourced transaction which if we observe we can see now has the signature_expiration_ledger
and signature
filled in.
With the transaction fully signed both externally for the basic submission fees and internally for all individual Soroban auth entries and the resources all appropriately set we can submit the transaction successfully. Yay!
So that’s how require_auth
works. And really require_auth_for_args
isn’t any different, it just allows you to specifically set the values for the root_invocation.function.contract_fn.args
. You’ll notice by default these will simply mirror the args passed to the contract function invocation itself, so in our case that source
Address
. If however we wanted to customize these values we’d use require_auth_for_args
. Let’s turn to the ./contracts/authplore_2/src/lib.rs
to demonstrate.
#![no_std]
use soroban_sdk::{contract, contractimpl, vec, Address, Env, IntoVal};
#[contract]
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn run(env: Env, source: Address) -> bool {
source.require_auth_for_args(vec![
&env,
u32::MAX.into_val(&env),
u64::MAX.into_val(&env),
u128::MAX.into_val(&env),
]);
true
}
}
mod test;
So here you’ll see instead of assuming the run
args (just source
) we’re going to set our own args. (u32::MAX
, u64::MAX
and u128::MAX
). What we would expect to see then for root_invocation.function.contract_fn.args
would be these 3 values. Let’s observe by running index_3.ts
.
bun run index_3.ts
{
"credentials": "source_account",
"root_invocation": {
"function": {
"contract_fn": {
"contract_address": "CAYEVKWBNZ7TUOY4KHGOCPXZ7H4D5OY5QHYD6GIPBKPXTQXCF5Y576PE",
"function_name": "run",
"args": [
{
"u32": 4294967295
},
{
"u64": 18446744073709552000
},
{
"u128": {
"hi": 18446744073709552000,
"lo": 18446744073709552000
}
}
]
}
},
"sub_invocations": []
}
}
SUCCESS true
Observe especially this block
...
"args": [
{
"u32": 4294967295
},
{
"u64": 18446744073709552000
},
{
"u128": {
"hi": 18446744073709552000,
"lo": 18446744073709552000
}
}
]
...
Exactly what we expected since we manually set the args for the auth entry. Everything in this index_3.ts
is exactly the same as index_1.ts
. Only the contract id is different. In 3 we’re pointing to the authplore_2
contract instead of authplore_1
.
Awesome! Entirely composable yet super simple authentication. Before we pack up and move on however let’s explore one final important note and really the reason I’m writing this article in the first place as it’s caught me by surprise a couple times. There’s a slight gotcha that can happen due to the fact we’re making heavy use of simulation. If we end up signing for values that can change between simulation and submission. Let’s illustrate by adding a random value to the require_auth_for_args
Vec
. It’ll be in authplore_3/src/lib.rs
.
#![no_std]
use soroban_sdk::{contract, contractimpl, vec, Address, Env, IntoVal};
#[contract]
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn run(env: Env, source: Address) -> bool {
source.require_auth_for_args(vec![
&env,
env.prng().gen::<u64>().into_val(&env),
u32::MAX.into_val(&env),
u64::MAX.into_val(&env),
u128::MAX.into_val(&env),
]);
true
}
}
mod test;
We’re really only adding one item to the require_auth_for_args
→ env.prng().gen::<u64>().into_val(&env),
Let’s run this in ./index_4.ts
bun run index_4.ts
{
"credentials": "source_account",
"root_invocation": {
"function": {
"contract_fn": {
"contract_address": "CBNMG5A6PT7IO6H44R7FD5VYKTIHQFGYFEOHB7NIGPVVQGJWJPT4GFMT",
"function_name": "run",
"args": [
{
"u64": 451945084965613630
},
{
"u32": 4294967295
},
{
"u64": 18446744073709552000
},
{
"u128": {
"hi": 18446744073709552000,
"lo": 18446744073709552000
}
}
]
}
},
"sub_invocations": []
}
}
FAILED true
{
status: "FAILED",
latestLedger: 9463,
latestLedgerCloseTime: "1711042625",
oldestLedger: 8024,
oldestLedgerCloseTime: "1711041171",
applicationOrder: 1,
envelopeXdr: "AAAAAgAAAACCFboXH3KO2VTJDerLz/MbdA9zkbX2M5TO5yH89TGROgAAoe4AACB5AAAADwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABWsN0Hnz+h3j85H5R9rhU0HgU2CkccP2oM+tYGTZL58MAAAADcnVuAAAAAAEAAAASAAAAAAAAAACCFboXH3KO2VTJDerLz/MbdA9zkbX2M5TO5yH89TGROgAAAAEAAAAAAAAAAAAAAAFaw3QefP6HePzkflH2uFTQeBTYKRxw/agz61gZNkvnwwAAAANydW4AAAAABAAAAAUGRaGpB1vwTQAAAAP/////AAAABf//////////AAAACf////////////////////8AAAAAAAAAAQAAAAAAAAACAAAABgAAAAFaw3QefP6HePzkflH2uFTQeBTYKRxw/agz61gZNkvnwwAAABQAAAABAAAAB9G4pXg0qvCWBMqgCr4siMQBXDznS8BrhTmuIijI+oubAAAAAAA6ubQAAAMkAAAAAAAAAAAAAKGKAAAAAfUxkToAAABA2MymZlJaq8itflrsj1UQvSkPRwqJZIGHLDerfvA5Mz1Tv08JhF3my4RaArZIY9d4pisdh6/SyKjcHeytjgVVDg==",
resultXdr: "AAAAAAAAkKn/////AAAAAQAAAAAAAAAY/////gAAAAA=",
resultMetaXdr: "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",
ledger: 9460,
createdAt: "1711042622",
}
Oh lord 😳
What’s going on here? If you decode the resultMetaXdr
you’ll see some errors like:
invalid_action
Unauthorized function call for address
escalating error to VM trap from failed host function call: require_auth_for_args
VM call trapped with HostError
host_fn_failed
Seems like something’s up with our authentication. It’s a bit tough to tease it out but painful experience tells me there’s a difference in the auth verification on the signed data in the simulation vs the submission. In short simulation generates a different random env.prng()
value than submission does. That’s an oof. If we seed the prng though to ensure there aren’t differences this error will go away. Simulation has to match submission. Once it does we’ll be golden. I do this in authplore_4
and then use that in index_5.ts
.
We add this line in the contract to seed the prng:
env.prng().seed(source.clone().to_xdr(&env).slice(..32));
And then we run the app.
bun run index_5.ts
{
"credentials": "source_account",
"root_invocation": {
"function": {
"contract_fn": {
"contract_address": "CCDKP2J6URPE2RTXOBDPVH3WXCVFM6HX54XWYCIKHX7SWU2USFHPCSQL",
"function_name": "run",
"args": [
{
"u64": 14907397655049556000
},
{
"u32": 4294967295
},
{
"u64": 18446744073709552000
},
{
"u128": {
"hi": 18446744073709552000,
"lo": 18446744073709552000
}
}
]
}
},
"sub_invocations": []
}
}
SUCCESS true
Sweet! It’s probably not very practical or useful to seed a prng with an account’s bytes as the “random” value will now always be the same for this contract when called with the same source
but it serves the purpose of proving the point that there’s both lag and rng differences between simulation and submission and as it relates to contract invocations and auth this will be important to keep in mind. If your auth or storage keys are utilizing variable, random values you may be in for a world of hurt.
And so now you know all about require_auth
and require_auth_for_args
and what’s going on under the hood for appropriately utilizing these methods on both the client and contract sides.