En Solidity, msg.sender es una variable global que representa la dirección que llamó o inició una llamada a una función en un contrato inteligente. La variable global tx.origin es la billetera que firmó la transacción.
En Solana, no hay un equivalente a msg.sender.
Hay un equivalente a tx.origin pero debes tener en cuenta que las transacciones en Solana pueden tener múltiples firmantes, por lo que podríamos pensar que tienen “múltiples tx.origin”.
Para obtener la dirección de “tx.origin” en Solana, necesitas configurarla añadiendo la cuenta Signer al contexto de la función y pasar la cuenta del llamante a esta cuando se llame a la función.
Veamos un ejemplo de cómo podemos acceder a la dirección del firmante de la transacción en Solana:
use anchor_lang::prelude::*;
declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");
#[program]
pub mod day14 {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
// Function logic....
msg!("The signer1: {:?}", *the_signer1.key);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub signer1: Signer<'info>,
}
Del fragmento de código anterior, Signer<'info> se utiliza para verificar que la cuenta signer1 en el struct de cuenta Initialize<'info> haya firmado la transacción.
En la función initialize, la cuenta signer1 se referencia de forma mutable desde el contexto y se asigna a la variable the_signer1.
Por último, registramos la clave pública (dirección) de signer1 utilizando la macro msg! y pasando *the_signer1.key, lo cual desreferencia y accede al campo o método key en el valor real al que apunta the_signer1.
Lo siguiente es escribir una prueba para el programa anterior:
describe("Day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
it("Is signed by a single signer", async () => {
// Add your test here.
const tx = await program.methods.initialize().accounts({
signer1: program.provider.publicKey
}).rpc();
console.log("The signer1: ", program.provider.publicKey.toBase58());
});
});
En la prueba, pasamos nuestra cuenta de billetera como firmante a la cuenta signer1, y luego llamamos a la función initialize. Después de eso, registramos la cuenta de la billetera en la consola para verificar su consistencia con la de nuestro programa.
Ejercicio: ¿Qué notaste en las salidas de shell_1 (terminal de comandos) y shell_3 (terminal de registros) después de ejecutar la prueba?
Múltiples firmantes
En Solana, también podemos tener más de un firmante autorizando una transacción; puedes pensar en esto como agrupar un montón de firmas y enviarlas en una sola transacción. Un caso de uso es realizar una transacción multifirma en una sola transacción.
Para hacer eso, simplemente agregamos más structs Signer al struct de la cuenta en nuestro programa, y luego nos aseguramos de que se pasen las cuentas necesarias al llamar a la función:
use anchor_lang::prelude::*;
declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");
#[program]
pub mod day14 {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
let the_signer2: &mut Signer = &mut ctx.accounts.signer2;
msg!("The signer1: {:?}", *the_signer1.key);
msg!("The signer2: {:?}", *the_signer2.key);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
pub signer1: Signer<'info>,
pub signer2: Signer<'info>,
}
El ejemplo anterior es en cierta forma igual al ejemplo de un solo firmante, con una diferencia notable. En este caso, agregamos otra cuenta Signer (signer2) al struct Initialize y también registramos las claves públicas de ambos firmantes en la función initialize.
Llamar a la función initialize con múltiples firmantes es diferente, en comparación con un solo firmante. La prueba a continuación muestra cómo invocar una función con múltiples firmantes:
describe("Day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
// generate a signer to call our function
let myKeypair = anchor.web3.Keypair.generate();
it("Is signed by multiple signers", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
signer1: program.provider.publicKey,
signer2: myKeypair.publicKey,
})
.signers([myKeypair])
.rpc();
console.log("The signer1: ", program.provider.publicKey.toBase58());
console.log("The signer2: ", myKeypair.publicKey.toBase58());
});
});
Entonces, ¿qué es diferente en la prueba anterior? Primero está el método signers(), que toma como argumento un arreglo de firmantes que firman una transacción. Pero solo tenemos un firmante en el arreglo, en lugar de dos. Anchor pasa automáticamente la cuenta de la billetera en el proveedor como firmante, por lo que no necesitamos agregarla nuevamente al arreglo de firmantes.
Generando direcciones aleatorias para hacer pruebas
El segundo cambio es la variable myKeypair, que almacena el Keypair (Una clave pública y su correspondiente clave privada para acceder a una cuenta) que se genera aleatoriamente mediante el módulo anchor.web3. En la prueba, asignamos la clave pública del Keypair (que se almacena en la variable myKeypair) a la cuenta signer2, por eso se pasa como argumento en el método .signers([myKeypair]).
Ejecuta la prueba varias veces y notarás que la clave pública de signer1 no cambia, pero la clave pública de signer2 sí. Esto se debe a que la cuenta de la billetera asignada a la cuenta signer1 (en la prueba) proviene del proveedor, que también es la cuenta de la billetera de Solana en tu máquina local, y la cuenta asignada a signer2 se genera aleatoriamente cada vez que ejecutas anchor test —skip-local-validator.
Ejercicio: Crea otra función (puedes llamarla como quieras) que requiera tres firmantes (la cuenta de la billetera del proveedor y dos cuentas generadas aleatoriamente) y escribe una prueba para ella.
onlyOwner
Este es un patrón común utilizado en Solidity para restringir el acceso a una función únicamente al propietario del contrato. Usando el atributo #[access_control] de Anchor, también podemos implementar el patrón de propietario único (only owner), es decir, restringir el acceso a una función en nuestro programa de Solana a una Pubkey (dirección del propietario).
Aquí hay un ejemplo de cómo implementar la funcionalidad “onlyOwner” en Solana:
use anchor_lang::prelude::*;
declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");
// NOTE: Replace with your wallet's public key
const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";
#[program]
pub mod day14 {
use super::*;
#[access_control(check(&ctx))]
pub fn initialize(ctx: Context<OnlyOwner>) -> Result<()> {
// Function logic...
msg!("Holla, I'm the owner.");
Ok(())
}
}
fn check(ctx: &Context<OnlyOwner>) -> Result<()> {
// Check if signer === owner
require_keys_eq!(
ctx.accounts.signer_account.key(),
OWNER.parse::<Pubkey>().unwrap(),
OnlyOwnerError::NotOwner
);
Ok(())
}
#[derive(Accounts)]
pub struct OnlyOwner<'info> {
signer_account: Signer<'info>,
}
// An enum for custom error codes
#[error_code]
pub enum OnlyOwnerError {
#[msg("Only owner can call this function!")]
NotOwner,
}
En el contexto del código anterior, la variable OWNER almacena la clave pública (dirección) asociada con mi billetera local de Solana. Asegúrate de reemplazar la variable OWNER con la clave pública de tu billetera antes de realizar las pruebas. Puedes obtener fácilmente tu clave pública ejecutando el comando solana address.
El atributo #[access_control] ejecuta el método de control de acceso dado antes de ejecutar la instrucción principal. Cuando se llama a la función initialize, el método de control de acceso (check) se ejecuta antes que la función initialize. El método check acepta un contexto referenciado como argumento, y luego verifica si el firmante de la transacción es igual al valor de la variable OWNER. La macro require_keys_eq! asegura que los valores de dos claves públicas sean iguales; si es cierto, ejecuta la función initialize, de lo contrario, se revierte con el error personalizado NotOwner.
Probando la funcionalidad onlyOwner — caso feliz
En la prueba a continuación, estamos llamando a la función initialize y firmando la transacción usando el Keypair del propietario:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Day14 } from "../target/types/day14";
describe("day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
it("Is called by the owner", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
signerAccount: program.provider.publicKey,
})
.rpc();
console.log("Transaction hash:", tx);
});
});
Llamamos a la función initialize y pasamos la cuenta de la billetera (cuenta de billetera local de Solana) en el proveedor al signerAccount que tiene el struct Signer<'info>, para validar que la cuenta de la billetera realmente haya firmado la transacción. También recuerda que Anchor firma secretamente cualquier transacción utilizando la cuenta de la billetera en el proveedor.
Ejecuta la prueba con anchor test --skip-local-validator; si todo se hizo correctamente, la prueba debería pasar:

Probando si el firmante no es el propietario — caso de ataque
Usar un Keypair diferente que no sea el del propietario para llamar a la función initialize y firmar la transacción arrojará un error, ya que la llamada a la función está restringida únicamente al propietario:
describe("day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
let Keypair = anchor.web3.Keypair.generate();
it("Is NOT called by the owner", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
signerAccount: Keypair.publicKey,
})
.signers([Keypair])
.rpc();
console.log("Transaction hash:", tx);
});
});
Aquí generamos un Keypair aleatorio y lo usamos para firmar la transacción. Ejecutemos la prueba nuevamente:

Como era de esperar, obtuvimos un error, ya que la clave pública del firmante no es igual a la clave pública del propietario.
Modificar el propietario
Para cambiar al propietario en un programa, la clave pública asignada al propietario debe almacenarse en la cadena (on-chain). Sin embargo, las discusiones sobre el “almacenamiento” en Solana se cubrirán en un futuro tutorial.
El propietario simplemente puede volver a desplegar el bytecode.
Ejercicio: Actualiza un programa como el anterior para tener un nuevo propietario.
Aprende más con RareSkills
Este tutorial es el capítulo 14 de nuestro curso de Solana.
Publicado originalmente el 21 de febrero de 2024