Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: SNS Realloc Name Account #3955

Merged
merged 18 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
joncinque marked this conversation as resolved.
Show resolved Hide resolved
}

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