Skip to main content

🔐 保障我们程序的安全

🎯 本节目标

嘿,开发者们! 👋 是时候给我们的Solana电影数据库程序穿上一件防弹衣了!🦺 在这一节中,我们将把一个普通的程序变成一个铜墙铁壁般的安全堡垒。

🎪 今天的精彩节目包括:

  • 🛡️ 安全防护升级 - 让黑客无处下手!
  • 输入验证大法 - 垃圾数据?门都没有!
  • 🔄 更新功能 - 让用户可以修改他们的影评
  • 💡 最佳实践 - 专业开发者的秘密武器

🚀 快速开始

想要立即开始编码?点击这个 ⚡ Playground魔法传送门 一键启动!

📁 项目文件结构一览

📦 movie-review-program
┣ 📜 lib.rs # 📚 模块注册中心
┣ 📜 entrypoint.rs # 🚪 程序的大门
┣ 📜 instruction.rs # 📨 指令的邮局
┣ 📜 processor.rs # 🧠 处理逻辑的大脑
┣ 📜 state.rs # 💾 状态存储仓库
┗ 📜 error.rs # ⚠️ 错误处理专家(新成员!)

🔧 初始配置调整

📐 固定账户大小 - 告别动态烦恼!

processor.rs 中,我们要做一个聪明的改变

// 🎯 在 account_len 函数里
// ❌ 旧方式:动态计算(麻烦且容易出错)
// let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());

// ✅ 新方式:固定大小(简单粗暴有效!)
let account_len: usize = 1000; // 💡 足够大,省心省力!

💡 Pro Tip: 固定大小意味着更新评论时不用重新计算租金,这就像买了个大房子,再也不用担心装不下新家具了!

🏗️ 状态管理升级

state.rs 中添加一些魔法咒语

// 🎭 实现 Sealed 特性 - 给编译器一个优化的机会
impl Sealed for MovieAccountState {}

// 🔍 实现初始化检查 - 防止操作未初始化的账户
impl IsInitialized for MovieAccountState {
fn is_initialized(&self) -> bool {
self.is_initialized // 返回初始化标志
}
}

🚨 自定义错误系统 - 让错误信息更友好!

📝 错误场景清单

想象一下这些灾难场景

  • 😱 用户试图更新一个不存在的评论
  • 🎭 有人伪造了PDA地址
  • 📏 评论内容比《战争与和平》还长
  • ⭐ 有人想给电影打100颗星(虽然热情可嘉,但不符合规则)

🎨 创建专属错误类型

error.rs 中,让我们创建一个错误艺术馆

use solana_program::program_error::ProgramError;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ReviewError {
// 错误 0️⃣ - 账户还在睡觉
#[error("Account is not initialized yet! 😴")]
UninitializedAccount,

// 错误 1️⃣ - PDA身份证不匹配
#[error("PDA mismatch! This is not the droid you're looking for 🤖")]
InvalidPDA,

// 错误 2️⃣ - 数据太长了
#[error("Data is too long! Keep it concise, please 📏")]
InvalidDataLength,

// 错误 3️⃣ - 评分不合理
#[error("Rating must be between 1-5 stars! ⭐")]
InvalidRating,
}

// 🔄 转换魔法 - 让自定义错误变成程序错误
impl From<ReviewError> for ProgramError {
fn from(e: ReviewError) -> Self {
ProgramError::Custom(e as u32)
}
}

别忘了在 processor.rs 中引入我们的新朋友:

// 🎯 在 processor.rs 顶部
use crate::error::ReviewError;

🛡️ 强化 add_movie_review 函数

1️⃣ 签名验证 - 确认身份!

// 🔍 获取账户信息
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;

// ✍️ 检查签名 - 没签名?没门!
if !initializer.is_signer {
msg!("🚫 Hey! You forgot to sign! No signature, no service!");
return Err(ProgramError::MissingRequiredSignature)
}

2️⃣ PDA验证 - 防伪认证!

// 🔐 生成预期的PDA
let (pda, bump_seed) = Pubkey::find_program_address(
&[initializer.key.as_ref(), title.as_bytes().as_ref()],
program_id
);

// 🎯 验证PDA是否匹配
if pda != *pda_account.key {
msg!("❌ PDA doesn't match! Nice try, but no cigar!");
return Err(ProgramError::InvalidArgument)
}

3️⃣ 数据验证 - 质量把关!

// ⭐ 检查评分范围(1-5星)
if rating > 5 || rating < 1 {
msg!("🌟 Rating must be 1-5 stars! We're not Michelin!");
return Err(ReviewError::InvalidRating.into())
}

// 📏 检查数据长度
let total_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
if total_len > 1000 {
msg!("📚 Your review is longer than a novel! Please keep it under 1000 bytes!");
return Err(ReviewError::InvalidDataLength.into())
}

🎯 Fun Fact: 为什么是1000字节?因为这足够写一篇精彩的影评,但又不会让区块链变成图书馆!


🆕 实现更新功能 - 让用户改变主意!

📋 第一步:更新指令枚举

instruction.rs 中添加新变体:

pub enum MovieInstruction {
// 🎬 添加新评论
AddMovieReview {
title: String,
rating: u8,
description: String
},
// ✏️ 更新已有评论(新功能!)
UpdateMovieReview {
title: String,
rating: u8,
description: String
}
}

🎯 第二步:解包逻辑升级

impl MovieInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// 🎪 分离变体类型和数据
let (&variant, rest) = input.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;

// 📦 解析负载数据
let payload = MovieReviewPayload::try_from_slice(rest).unwrap();

// 🎭 根据变体类型返回相应指令
Ok(match variant {
0 => Self::AddMovieReview { // 🆕 新增
title: payload.title,
rating: payload.rating,
description: payload.description
},
1 => Self::UpdateMovieReview { // ✏️ 更新
title: payload.title,
rating: payload.rating,
description: payload.description
},
_ => {
msg!("❓ Unknown instruction variant!");
return Err(ProgramError::InvalidInstructionData)
}
})
}
}

🎮 第三步:处理器路由

pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8]
) -> ProgramResult {
// 📨 解包指令
let instruction = MovieInstruction::unpack(instruction_data)?;

// 🚦 路由到对应的处理函数
match instruction {
MovieInstruction::AddMovieReview { title, rating, description } => {
msg!("➕ Processing AddMovieReview...");
add_movie_review(program_id, accounts, title, rating, description)
},
MovieInstruction::UpdateMovieReview { title, rating, description } => {
msg!("✏️ Processing UpdateMovieReview...");
update_movie_review(program_id, accounts, title, rating, description)
}
}
}

🎨 实现 update_movie_review 函数

🏗️ 基础框架

pub fn update_movie_review(
program_id: &Pubkey,
accounts: &[AccountInfo],
_title: String, // 💡 注意:title带下划线,因为我们不会修改它
rating: u8,
description: String
) -> ProgramResult {
msg!("🎬 Lights, Camera, Update! Starting movie review update...");

// 🎯 获取账户迭代器
let account_info_iter = &mut accounts.iter();

// 📦 解包账户
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;

// 更多逻辑即将到来...
Ok(())
}

🔒 安全检查大礼包

// 1️⃣ 所有权检查 - 确保程序拥有这个账户
if pda_account.owner != program_id {
msg!("🚫 This account doesn't belong to our program!");
return Err(ProgramError::IllegalOwner)
}

// 2️⃣ 签名检查 - 确保是本人操作
if !initializer.is_signer {
msg!("✍️ Please sign your transaction!");
return Err(ProgramError::MissingRequiredSignature)
}

// 3️⃣ 解包账户数据
msg!("📦 Unpacking account data...");
let mut account_data = try_from_slice_unchecked::<MovieAccountState>(
&pda_account.data.borrow()
).unwrap();
msg!("✅ Account data unpacked successfully!");

🎯 深度验证

// 🔐 验证PDA
let (pda, _bump_seed) = Pubkey::find_program_address(
&[
initializer.key.as_ref(),
account_data.title.as_bytes().as_ref()
],
program_id
);

if pda != *pda_account.key {
msg!("❌ PDA validation failed!");
return Err(ReviewError::InvalidPDA.into())
}

// 🔍 检查账户是否已初始化
if !account_data.is_initialized() {
msg!("😴 Account is not initialized yet!");
return Err(ReviewError::UninitializedAccount.into());
}

// ⭐ 验证评分
if rating > 5 || rating < 1 {
msg!("🌟 Invalid rating! Must be 1-5 stars");
return Err(ReviewError::InvalidRating.into())
}

// 📏 检查数据长度
let total_len: usize = 1 + 1 + (4 + account_data.title.len()) + (4 + description.len());
if total_len > 1000 {
msg!("📚 Data too long! Maximum 1000 bytes");
return Err(ReviewError::InvalidDataLength.into())
}

💾 保存更新

// 🎨 更新数据
account_data.rating = rating;
account_data.description = description;

// 💾 序列化并保存
account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

msg!("🎉 Review updated successfully!");
Ok(())

🧪 测试你的杰作!

🛠️ 构建和部署

# 🔨 构建程序
cargo build-sbf

# 🚀 部署到链上
solana program deploy

# 📋 复制程序地址
echo "Don't forget to copy your program address!"

🎨 前端测试

# 📦 克隆前端项目
git clone https://github.com/all-in-one-solana/solana-movie-frontend

# 📁 进入项目目录
cd solana-movie-frontend

# 🎯 切换到更新功能分支
git checkout solution-update-reviews

# 📦 安装依赖
npm install

# 🚀 启动应用
npm run dev

🎉 成功啦! 现在你的前端应该可以:

  • 📝 添加新的电影评论
  • ✏️ 更新已有的评论
  • 🎨 展示所有评论

🏆 终极挑战 - 学生介绍项目

🎯 任务清单

现在轮到你大展身手了!拿起你的键盘,让我们升级学生介绍项目:

📋 必做任务:

  1. ➕ 添加更新功能

    • 允许学生修改他们的自我介绍
    • 保持名字不变,只更新留言内容
  2. 🔐 安全升级包

    • ✅ 签名验证
    • 🔍 PDA验证
    • 📏 数据长度检查
    • 🎯 初始化状态检查

🎁 加分项:

  1. 🌟 创意功能(可选)
    • 添加时间戳
    • 实现点赞功能
    • 添加标签系统

🚀 起始代码

从这里开始你的冒险:📦 起始代码传送门

💡 专业建议

🧠 智慧锦囊:

  • 先实现基础功能,再添加花哨的特性
  • 每添加一个检查,都要写对应的测试
  • 错误信息要友好且有帮助
  • 记得给你的代码添加有趣的注释!

🏁 卡住了?

别担心!这里有一份参考答案:🎯 解决方案

但是记住:

  • 🎨 你的实现可能和答案不同,那也很棒!
  • 💡 重要的是理解概念,而不是复制代码
  • 🚀 创新和改进永远受欢迎!

🎊 总结

恭喜你!🎉 你已经成功地:

  • 🔐 加固了程序安全
  • ✅ 实现了完整的验证系统
  • 🔄 添加了更新功能
  • 🧠 学会了最佳实践

🌟 下一步?

  • 尝试添加删除功能
  • 实现评论的评论(嵌套评论)
  • 创建一个评分排行榜
  • 天空才是你的极限!

记住: 安全的程序 = 快乐的用户 = 成功的项目! 🚀


💬 有问题? 在社区里提问,我们都在这里帮助你!

🔗 分享你的成果 在Twitter上 @我们,展示你的杰作!

祝编码愉快!Happy Coding! 🎉👨‍💻👩‍💻