Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.
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
233 changes: 189 additions & 44 deletions runtime/src/system_instruction_processor.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
use log::*;
use solana_sdk::account::KeyedAccount;
use solana_sdk::instruction::InstructionError;
use solana_sdk::instruction_processor_utils::{limited_deserialize, next_keyed_account};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::system_instruction::{SystemError, SystemInstruction};
use solana_sdk::system_program;
use solana_sdk::sysvar;

// 10 MB
const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024;

fn create_system_account(

use solana_sdk::{
account::KeyedAccount,
instruction::InstructionError,
instruction_processor_utils::{limited_deserialize, next_keyed_account},
pubkey::Pubkey,
system_instruction::{
create_address_with_seed, SystemError, SystemInstruction, MAX_PERMITTED_DATA_LENGTH,
},
system_program, sysvar,
};

fn create_account_with_seed(
from: &mut KeyedAccount,
to: &mut KeyedAccount,
seed: &str,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for the restriction to 32B ASCII string? [u8; 32] could support some internationalization and hash digests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah good point, we don't care that the seed is valid utf-8

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep the space smaller than 256 bytes of seed to deter grinding attacks. 32 ascii chars takes away 32 bits.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rob-solana grinding on what though?

Copy link
Copy Markdown
Contributor Author

@rob-solana rob-solana Dec 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trying to DoS a guy you think wants his authorized staker+"shrill conveyance 23/50"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rob-solana you can always grind with pubkey generation. I think we should either have a Hash/[u8;32], or u64.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requiring from's signature effectively makes griefing the address space a second-preimage attack on SHA256. We should probably be OK with that. Though fewer bits do minimize opportunities for shenanigans. Would it make sense to restrict the seed to something as small as a u16?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were it as small as a u16, mnemonics get harder. ascii is nice for mnemonics (in English). opening to utf-8 would make it look less amateur-hour.

The grinding attack is picking another source from (obs having the keypair), and griefing some other dude's from. The goal is to make that griefing look hard or futile.

I think we can do this with:

  1. u64, maybe low 8 bytes of hash(mnemonic string)?
  2. hashing a limited-length string as proposed. Upgrading to utf-8 hurts the deterrent a little as you can cover 256 bits of space with only 13-ish utf-8 chars (at 20bits/char).

We can also say "who cares? grind away!"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to remove the ascii stuff, appreciate input here @t-nelson @aeyakovenko @garious

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were it as small as a u16, mnemonics get harder. ascii is nice for mnemonics (in English). opening to utf-8 would make it look less amateur-hour.

Right, the u16 suggestion would discard the mnemonic for an index (from gets I accounts under program_id). Which gives cheap introspection, but no pseudonymity.

The grinding attack is picking another source from (obs having the keypair), and griefing some other dude's from. The goal is to make that griefing look hard or futile.

Yep. A SHA256 second preimage search is pretty futile IMO. Restricting free bits per from is about the most effective deterrent we can enact. Grinding froms is always possible and slower than the hashing step. You've already covered the other speedup. That is, requiring fixed bits at the end of the input data, preventing working from a midstate.

I think we can do this with:

1. u64, maybe low 8 bytes of hash(mnemonic string)?

2. hashing a limited-length string as proposed.  Upgrading to utf-8 hurts the deterrent a little as you can cover 256 bits of space with only 13-ish utf-8 chars (at 20bits/char).

I think (valid) utf-8 would actually be more restricted. For non-ASCII code points, the top two bits of every byte are fixed and the first byte also consumes up to three more bits to encode the length. Not all code pages are in use or full either, further restricting valid bitstrings

We can also say "who cares? grind away!"

Probably fine. See, https://lbc.cryptoguru.org/about

lamports: u64,
data_length: u64,
program_id: &Pubkey,
) -> Result<(), InstructionError> {
// if lamports == 0, the from account isn't touched
// `from` is the source of the derived address, the caller must have
// signed, even if no lamports will be transferred
if from.signer_key().is_none() {
debug!("CreateAccountWithSeed: from must sign");
return Err(InstructionError::MissingRequiredSignature);
}

// re-derive the address, must match `to`
let address = create_address_with_seed(from.unsigned_key(), seed, program_id)?;

if to.unsigned_key() != &address {
debug!(
"CreateAccountWithSeed: invalid argument; generated {} does not match to {}",
address,
to.unsigned_key()
);
return Err(SystemError::AddressWithSeedMismatch.into());
}

// all of finish_create_account's rules apply
finish_create_account(from, to, lamports, data_length, program_id)
}

fn create_account(
from: &mut KeyedAccount,
to: &mut KeyedAccount,
lamports: u64,
data_length: u64,
program_id: &Pubkey,
) -> Result<(), InstructionError> {
// if lamports == 0, the `from` account isn't touched
if lamports != 0 && from.signer_key().is_none() {
debug!("CreateAccount: from must sign");
return Err(InstructionError::MissingRequiredSignature);
Expand All @@ -28,7 +60,17 @@ fn create_system_account(
return Err(InstructionError::MissingRequiredSignature);
}

// if it looks like the to account is already in use, bail
finish_create_account(from, to, lamports, data_length, program_id)
}

fn finish_create_account(
from: &mut KeyedAccount,
to: &mut KeyedAccount,
lamports: u64,
data_length: u64,
program_id: &Pubkey,
) -> Result<(), InstructionError> {
// if it looks like the `to` account is already in use, bail
if to.account.lamports != 0
|| !to.account.data.is_empty()
|| !system_program::check_id(&to.account.owner)
Expand Down Expand Up @@ -61,11 +103,18 @@ fn create_system_account(
return Err(SystemError::InvalidAccountDataLength.into());
}

assign_account_to_program(to, program_id)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not calling assign_account_to_program anymore for normal accounts starts to skip this existing check for account.signer_key. Is that OK?

Copy link
Copy Markdown
Contributor Author

@rob-solana rob-solana Dec 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that check is already present in create_account() and is not enforced by create_account_with_seed()

this change was intentional

note that no tests needed to change, we're still requiring to signature in the non-seed case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for clarifying!

// guard against sysvars being assigned
if sysvar::check_id(&program_id) {
debug!("Assign: program id {} invalid", program_id);
return Err(SystemError::InvalidProgramId.into());
}
to.account.owner = *program_id;

from.account.lamports -= lamports;
to.account.lamports += lamports;
to.account.data = vec![0; data_length as usize];
to.account.executable = false;

Ok(())
}

Expand Down Expand Up @@ -134,7 +183,17 @@ pub fn process_instruction(
} => {
let from = next_keyed_account(keyed_accounts_iter)?;
let to = next_keyed_account(keyed_accounts_iter)?;
create_system_account(from, to, lamports, space, &program_id)
create_account(from, to, lamports, space, &program_id)
}
SystemInstruction::CreateAccountWithSeed {
seed,
lamports,
space,
program_id,
} => {
let from = next_keyed_account(keyed_accounts_iter)?;
let to = next_keyed_account(keyed_accounts_iter)?;
create_account_with_seed(from, to, &seed, lamports, space, &program_id)
}
SystemInstruction::Assign { program_id } => {
let account = next_keyed_account(keyed_accounts_iter)?;
Expand Down Expand Up @@ -163,38 +222,120 @@ mod tests {
use solana_sdk::transaction::TransactionError;

#[test]
fn test_create_system_account() {
fn test_create_account() {
let new_program_owner = Pubkey::new(&[9; 32]);
let from = Pubkey::new_rand();
let to = Pubkey::new_rand();
let mut from_account = Account::new(100, 0, &system_program::id());
let mut to_account = Account::new(0, 0, &Pubkey::default());

assert_eq!(
process_instruction(
&Pubkey::default(),
&mut [
KeyedAccount::new(&from, true, &mut from_account),
KeyedAccount::new(&to, true, &mut to_account)
],
&bincode::serialize(&SystemInstruction::CreateAccount {
lamports: 50,
space: 2,
program_id: new_program_owner
})
.unwrap()
),
Ok(())
);
assert_eq!(from_account.lamports, 50);
assert_eq!(to_account.lamports, 50);
assert_eq!(to_account.owner, new_program_owner);
assert_eq!(to_account.data, [0, 0]);
}

#[test]
fn test_create_account_with_seed() {
let new_program_owner = Pubkey::new(&[9; 32]);
let from = Pubkey::new_rand();
let seed = "shiny pepper";
let to = create_address_with_seed(&from, seed, &new_program_owner).unwrap();

let mut from_account = Account::new(100, 0, &system_program::id());
let mut to_account = Account::new(0, 0, &Pubkey::default());

assert_eq!(
process_instruction(
&Pubkey::default(),
&mut [
KeyedAccount::new(&from, true, &mut from_account),
KeyedAccount::new(&to, false, &mut to_account)
],
&bincode::serialize(&SystemInstruction::CreateAccountWithSeed {
seed: seed.to_string(),
lamports: 50,
space: 2,
program_id: new_program_owner
})
.unwrap()
),
Ok(())
);
assert_eq!(from_account.lamports, 50);
assert_eq!(to_account.lamports, 50);
assert_eq!(to_account.owner, new_program_owner);
assert_eq!(to_account.data, [0, 0]);
}

#[test]
fn test_create_account_with_seed_mismatch() {
let new_program_owner = Pubkey::new(&[9; 32]);
let from = Pubkey::new_rand();
let seed = "dull boy";
let to = Pubkey::new_rand();

let mut from_account = Account::new(100, 0, &system_program::id());
let mut to_account = Account::new(0, 0, &Pubkey::default());

assert_eq!(
create_system_account(
create_account_with_seed(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&to, true, &mut to_account),
&mut KeyedAccount::new(&to, false, &mut to_account),
seed,
50,
2,
&new_program_owner,
),
Ok(())
Err(SystemError::AddressWithSeedMismatch.into())
);
assert_eq!(from_account.lamports, 100);
assert_eq!(to_account, Account::default());
}
#[test]
fn test_create_account_with_seed_missing_sig() {
let new_program_owner = Pubkey::new(&[9; 32]);
let from = Pubkey::new_rand();
let seed = "dull boy";
let to = create_address_with_seed(&from, seed, &new_program_owner).unwrap();

let from_lamports = from_account.lamports;
let to_lamports = to_account.lamports;
let to_owner = to_account.owner;
let to_data = to_account.data.clone();
assert_eq!(from_lamports, 50);
assert_eq!(to_lamports, 50);
assert_eq!(to_owner, new_program_owner);
assert_eq!(to_data, [0, 0]);
let mut from_account = Account::new(100, 0, &system_program::id());
let mut to_account = Account::new(0, 0, &Pubkey::default());

assert_eq!(
create_account_with_seed(
&mut KeyedAccount::new(&from, false, &mut from_account),
&mut KeyedAccount::new(&to, false, &mut to_account),
seed,
50,
2,
&new_program_owner,
),
Err(InstructionError::MissingRequiredSignature)
);
assert_eq!(from_account.lamports, 100);
assert_eq!(to_account, Account::default());
}

#[test]
fn test_create_with_zero_lamports() {
// Attempt to create account with zero lamports
// create account with zero lamports tranferred
let new_program_owner = Pubkey::new(&[9; 32]);
let from = Pubkey::new_rand();
let mut from_account = Account::new(100, 0, &Pubkey::new_rand()); // not from system account
Expand All @@ -203,7 +344,7 @@ mod tests {
let mut to_account = Account::new(0, 0, &Pubkey::default());

assert_eq!(
create_system_account(
create_account(
&mut KeyedAccount::new(&from, false, &mut from_account), // no signer
&mut KeyedAccount::new(&to, true, &mut to_account),
0,
Expand Down Expand Up @@ -234,7 +375,7 @@ mod tests {
let mut to_account = Account::new(0, 0, &Pubkey::default());
let unchanged_account = to_account.clone();

let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&to, true, &mut to_account),
150,
Expand All @@ -255,7 +396,7 @@ mod tests {
let to_account_key = Pubkey::new_rand();

// Trying to request more data length than permitted will result in failure
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from_account_key, true, &mut from_account),
&mut KeyedAccount::new(&to_account_key, true, &mut to_account),
50,
Expand All @@ -269,7 +410,7 @@ mod tests {
);

// Trying to request equal or less data length than permitted will be successful
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from_account_key, true, &mut from_account),
&mut KeyedAccount::new(&to_account_key, true, &mut to_account),
50,
Expand All @@ -293,7 +434,7 @@ mod tests {
let mut owned_account = Account::new(0, 0, &original_program_owner);
let unchanged_account = owned_account.clone();

let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&owned_key, true, &mut owned_account),
50,
Expand All @@ -308,7 +449,7 @@ mod tests {

let mut owned_account = Account::new(10, 0, &Pubkey::default());
let unchanged_account = owned_account.clone();
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&owned_key, true, &mut owned_account),
50,
Expand All @@ -333,7 +474,7 @@ mod tests {
let unchanged_account = owned_account.clone();

// Haven't signed from account
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, false, &mut from_account),
&mut KeyedAccount::new(&owned_key, true, &mut owned_account),
50,
Expand All @@ -345,7 +486,7 @@ mod tests {
assert_eq!(owned_account, unchanged_account);

// Haven't signed to account
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&owned_key, false, &mut owned_account),
50,
Expand All @@ -357,7 +498,7 @@ mod tests {
assert_eq!(owned_account, unchanged_account);

// support creation/assignment with zero lamports (ephemeral account)
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, false, &mut from_account),
&mut KeyedAccount::new(&owned_key, true, &mut owned_account),
0,
Expand All @@ -379,7 +520,7 @@ mod tests {
let mut to_account = Account::default();

// fail to create a sysvar::id() owned account
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&to, true, &mut to_account),
50,
Expand All @@ -392,7 +533,7 @@ mod tests {
let mut to_account = Account::default();

// fail to create an account with a sysvar id
let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&to, true, &mut to_account),
50,
Expand All @@ -419,7 +560,7 @@ mod tests {
};
let unchanged_account = populated_account.clone();

let result = create_system_account(
let result = create_account(
&mut KeyedAccount::new(&from, true, &mut from_account),
&mut KeyedAccount::new(&populated_key, true, &mut populated_account),
50,
Expand Down Expand Up @@ -447,9 +588,13 @@ mod tests {
);

assert_eq!(
assign_account_to_program(
&mut KeyedAccount::new(&from, true, &mut from_account),
&new_program_owner,
process_instruction(
&Pubkey::default(),
&mut [KeyedAccount::new(&from, true, &mut from_account)],
&bincode::serialize(&SystemInstruction::Assign {
program_id: new_program_owner
})
.unwrap()
),
Ok(())
);
Expand Down
Loading