Skip to content

Commit

Permalink
Feat: SNS Realloc Name Account (solana-labs#3955)
Browse files Browse the repository at this point in the history
* add new realloc ix

* add tests

* update param order to be consistent with other builders

* add realloc ix to instructions.ts

* ts bindings for realloc

* ts linting

* fix logs and clippy arithmetic

* satisfy clippy

* Update name-service/program/src/instruction.rs

Co-authored-by: Jon Cinque <joncinque@pm.me>

* Update name-service/program/src/instruction.rs

Co-authored-by: Jon Cinque <joncinque@pm.me>

* docs nits

* fix import ordering

* Update name-service/program/tests/functional.rs

Co-authored-by: Jon Cinque <joncinque@pm.me>

* blockhash nits

* Update name-service/js/src/bindings.ts

Co-authored-by: Jon Cinque <joncinque@pm.me>

* fix hashed_name naming

Co-authored-by: Jon Cinque <joncinque@pm.me>
  • Loading branch information
2 people authored and HaoranYi committed Jul 19, 2023
1 parent 1a60315 commit 667ac57
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 2 deletions.
47 changes: 47 additions & 0 deletions name-service/js/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {
createInstruction,
deleteInstruction,
reallocInstruction,
transferInstruction,
updateInstruction,
} from './instructions';
Expand Down Expand Up @@ -217,3 +218,49 @@ export async function deleteNameRegistry(

return changeAuthoritiesInstr;
}

/**
* Realloc the name account space.
*
* @param connection The solana connection object to the RPC node
* @param name The name of the name account
* @param space The new space to be allocated
* @param payerKey The allocation cost payer if new space is larger than current or the refund destination if smaller
* @param nameClass The class of this name, if it exsists
* @param nameParent The parent name of this name, if it exists
* @returns
*/
export async function reallocNameAccount(
connection: Connection,
name: string,
space: number,
payerKey: PublicKey,
nameClass?: PublicKey,
nameParent?: PublicKey
): Promise<TransactionInstruction> {
const hashedName = await getHashedName(name);
const nameAccountKey = await getNameAccountKey(
hashedName,
nameClass,
nameParent
);

let nameOwner: PublicKey;
if (nameClass) {
nameOwner = nameClass;
} else {
nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey))
.owner;
}

const reallocInstr = reallocInstruction(
NAME_PROGRAM_ID,
SystemProgram.programId,
payerKey,
nameAccountKey,
nameOwner,
new Numberu32(space)
);

return reallocInstr;
}
41 changes: 41 additions & 0 deletions name-service/js/src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,44 @@ export function deleteInstruction(
data,
});
}

export function reallocInstruction(
nameProgramId: PublicKey,
systemProgramId: PublicKey,
payerKey: PublicKey,
nameAccountKey: PublicKey,
nameOwnerKey: PublicKey,
space: Numberu32
): TransactionInstruction {
const buffers = [Buffer.from(Int8Array.from([4])), space.toBuffer()];

const data = Buffer.concat(buffers);
const keys = [
{
pubkey: systemProgramId,
isSigner: false,
isWritable: false,
},
{
pubkey: payerKey,
isSigner: true,
isWritable: true,
},
{
pubkey: nameAccountKey,
isSigner: false,
isWritable: true,
},
{
pubkey: nameOwnerKey,
isSigner: true,
isWritable: false,
},
];

return new TransactionInstruction({
keys,
programId: nameProgramId,
data,
});
}
40 changes: 40 additions & 0 deletions name-service/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ pub enum NameRegistryInstruction {
/// 2. `[writeable]` Refund account
///
Delete,

/// Realloc the data of a name record.
///
/// The space change cannot be more than `MAX_PERMITTED_DATA_LENGTH` greater than
/// current `space`.
///
/// Accounts expected by this instruction:
/// 0. `[]` System program
/// 1. `[writeable, signer]` Payer account (will be refunded if new `space` is less than current `space`)
/// 2. `[writeable]` Name record to be reallocated
/// 3. `[signer]` Account owner
///
Realloc {
/// New total number of bytes in addition to the `NameRecordHeader`.
/// There are no checks on the existing data; it will be truncated if the new space is less than the current space.
space: u32,
},
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -201,3 +218,26 @@ pub fn delete(
data,
})
}

pub fn realloc(
name_service_program_id: Pubkey,
payer_key: Pubkey,
name_account_key: Pubkey,
name_owner_key: Pubkey,
space: u32,
) -> Result<Instruction, ProgramError> {
let instruction_data = NameRegistryInstruction::Realloc { space };
let data = instruction_data.try_to_vec().unwrap();
let accounts = vec![
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new(payer_key, true),
AccountMeta::new(name_account_key, false),
AccountMeta::new_readonly(name_owner_key, true),
];

Ok(Instruction {
program_id: name_service_program_id,
accounts,
data,
})
}
58 changes: 58 additions & 0 deletions name-service/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ use {
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
},
std::cmp::Ordering,
};

pub struct Processor {}
Expand Down Expand Up @@ -239,6 +242,57 @@ impl Processor {
Ok(())
}

fn process_realloc(accounts: &[AccountInfo], space: u32) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let system_program = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let name_account = next_account_info(accounts_iter)?;
let name_owner = next_account_info(accounts_iter)?;

let name_record_header = NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;

// Verifications
if !name_owner.is_signer || name_record_header.owner != *name_owner.key {
msg!("The given name owner is incorrect or not a signer.");
return Err(ProgramError::InvalidArgument);
}

let new_space = NameRecordHeader::LEN.saturating_add(space as usize);
let required_lamports = Rent::get()?.minimum_balance(new_space);
match name_account.lamports().cmp(&required_lamports) {
Ordering::Less => {
// Overflow cannot happen here because we already checked the sizes.
#[allow(clippy::integer_arithmetic)]
let lamports_to_add = required_lamports - name_account.lamports();
invoke(
&system_instruction::transfer(
payer_account.key,
name_account.key,
lamports_to_add,
),
&[
payer_account.clone(),
name_account.clone(),
system_program.clone(),
],
)?;
}
Ordering::Greater => {
// Overflow cannot happen here because we already checked the sizes.
#[allow(clippy::integer_arithmetic)]
let lamports_to_remove = name_account.lamports() - required_lamports;
let source_amount: &mut u64 = &mut name_account.lamports.borrow_mut();
let dest_amount: &mut u64 = &mut payer_account.lamports.borrow_mut();
*source_amount = source_amount.saturating_sub(lamports_to_remove);
*dest_amount = dest_amount.saturating_add(lamports_to_remove);
}
Ordering::Equal => {}
}
// Max data increase is checked in realloc. No need to check here.
name_account.realloc(new_space, false)?;
Ok(())
}

pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
Expand Down Expand Up @@ -270,6 +324,10 @@ impl Processor {
msg!("Instruction: Delete Name");
Processor::process_delete(accounts)?;
}
NameRegistryInstruction::Realloc { space } => {
msg!("Instruction: Realloc Name Record");
Processor::process_realloc(accounts, space)?;
}
}
Ok(())
}
Expand Down
59 changes: 57 additions & 2 deletions name-service/program/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
use std::str::FromStr;

use solana_program::{instruction::Instruction, program_pack::Pack, pubkey::Pubkey};
use solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext};
use solana_program_test::{
processor, tokio, ProgramTest, ProgramTestBanksClientExt, ProgramTestContext,
};

use solana_program::hash::hashv;
use solana_sdk::{
Expand All @@ -11,7 +13,7 @@ use solana_sdk::{
transport::TransportError,
};
use spl_name_service::{
instruction::{create, delete, transfer, update, NameRegistryInstruction},
instruction::{create, delete, realloc, transfer, update, NameRegistryInstruction},
processor::Processor,
state::{get_seeds_and_key, NameRecordHeader, HASH_PREFIX},
};
Expand Down Expand Up @@ -175,6 +177,59 @@ async fn test_name_service() {
.unwrap();
println!("Name Record Header: {:?}", name_record_header);

let data = "hello".as_bytes().to_vec();
let update_instruction = update(
program_id,
space as u32,
data,
name_account_key,
sol_subdomains_class.pubkey(),
Some(name_record_header.parent_name),
)
.unwrap();

sign_send_instruction(
&mut ctx,
update_instruction.clone(),
vec![&sol_subdomains_class],
)
.await
.unwrap_err();

let new_space = space.checked_mul(2).unwrap();
let payer_key = ctx.payer.pubkey();
let realloc_instruction = |space| {
realloc(
program_id,
payer_key,
name_account_key,
payer_key,
space as u32,
)
.unwrap()
};

sign_send_instruction(&mut ctx, realloc_instruction(new_space), vec![])
.await
.unwrap();

// update blockhash to prevent losing txn to dedup
ctx.last_blockhash = ctx
.banks_client
.get_new_latest_blockhash(&ctx.last_blockhash)
.await
.unwrap();

// resend update ix. Should succeed this time.
sign_send_instruction(&mut ctx, update_instruction, vec![&sol_subdomains_class])
.await
.unwrap();

// realloc to smaller this time
sign_send_instruction(&mut ctx, realloc_instruction(space / 2), vec![])
.await
.unwrap();

let delete_instruction = delete(
program_id,
name_account_key,
Expand Down

0 comments on commit 667ac57

Please sign in to comment.