https://bakjuna.tistory.com/135?category=816799 

 

[solana] 솔라나 스마트 컨트랙트 만들어보기

수정: 실제로 현업에서 작동하는 프로그램을 제작하시려면 https://bakjuna.tistory.com/143 여길 참조해주세요! 이 글에선 솔라나 네이티브 코드로 제작하는 법을 설명하고 있습니다. 1. 들어가며 솔라

bakjuna.tistory.com

 

수정: 실제로 현업에서 작동하는 프로그램을 제작하시려면 https://bakjuna.tistory.com/143 여길 참조해주세요! 이 글에선 솔라나 네이티브 코드로 제작하는 법을 설명하고 있습니다.

1. 들어가며

솔라나는 최근 아주 각광받는 레이어1 코인입니다. 일단 gas fee가 이더리움에 비해 압도적으로 저렴하고, PoH라는 새로운 접근 방식을 통해 초당 트랜잭션 수도 아주 뛰어나 무려 초당 5만 개에 달하는 트랜잭션을 처리할 수 있습니다. 기존 비트코인, 이더리움의 초당 10여 개에 불과한 트랜잭션 수와 비교하면 아주 장족의 발전이죠.

 

트랜잭션의 gas fee도 현재 5000Lamport (0.000000001SOL) 에 불과해 20만원이 1솔라나라고 가정하면 트랜잭션당 소모되는 gas fee가 10원에 불과합니다. 해외 송금 해보신 분들은 아실거에요. 이 수치가 얼마나 대단한 것인지... 순식간에 이체되고, 아주 적은 비용으로 송금이 가능합니다.

 

솔라나는 당연히 최신 기술인 NFT 민팅과 Smart Contract, Defi 등을 지원합니다. 이번 글에선, 이 모든 기술들의 근간이 될 스마트 컨트랙트를 빌딩하는 방법에 대해서 알아보려고 합니다.

 

2. 스마트 컨트랙트란?

사실 많은 분들이 스마트 컨트랙트가 뭔지 구체적으로 알지 못하고 계시지만, 개념은 엄청 간단합니다. 그냥 자판기를 생각하시면 됩니다. 내가 어떤 일을 하면 어떤 결과가 나올지 미리 정해진 스크립트에 따라 수행되는 것이죠! 인터넷에 수행될 코드를 미리 업로드시켜놓고, 수행 조건이 만족되면 해당 코드가 자동으로 실행되고 연산됩니다. 이게 답니다!

 

그럼 이게 왜 혁신적이냐고요? 왜냐면, 기존에는 다들 '신뢰'에 기초해서 하던 작업들을 이젠 딱히 상대를 신뢰하지 않아도 되거든요. 예를 들면 제가 어떤 게임을 하고 이겼을 때 1000원을 받는다고 해봅시다. 종전에는 이 게임을 하고 1000원을 받는 행위 자체를 해당 사이트를 '신뢰'하지 않으면 할 수 없었습니다. 게임 사이트가 나에게 돈을 줄지, 주지 않을지 어떻게 아나요?

 

하지만 블록체인 스마트 컨트랙트는 다릅니다. 스마트 컨트랙트 온체인에서 일어나는 모든 일들은 스크립트를 읽을 수 있는 능력만 있다면 게임사이트가 특정 조건을 만족하면 자동으로 나에게 돈을 준다는 사실을 알 수 있습니다. 그렇지 않다면, 교묘하게 그렇지 않은 코드를 삽입했다는 사실도 알아낼 수 있죠. 현재는 일부 프로그래머만이 해당 사실을 검증할 수 있겠습니다만, 적어도 모두에게 공개되어있는 코드에 대놓고 스캠 코드를 삽입할 수는 없겠죠?

 

스마트 컨트랙트에 대한 대략적인 설명입니다. 먼저 프로그램된 컨트랙트 (계약)을 올려놓고, 이벤트가 진행이 되면 해당 이벤트가 실행이 됩니다. 그 실행 내역에 따라서 토큰 (뭐... 돈이죠) 이 오가는 것을 보증할 수 있습니다.

 

3. 스마트 컨트랙트 예제 살펴보기

3.1) 설치

 

솔라나 스마트 컨트랙트 예제를 살펴봅시다. 솔라나는 스마트 컨트랙트는 러스트로 짜여져 있고, 소통은 node로 할 수 있습니다.

 

 

  git clone git@github.com:solana-labs/example-helloworld.git
  sh -c "$(curl -sSfL https://release.solana.com/v1.9.1/install)"

 

이 프로젝트를 받아봅시다. 솔라나의 예제 프로젝트에요. 그리고, 솔라나도 한번 깔아봅시다. 두 번째 줄이 솔라나 CLI를 설치하는 구문입니다.

 

설치하고 나서 한번 살펴볼까요?

 

 

  solana config set --url https://api.devnet.solana.com

 

솔라나 데브넷으로 설정해보면, 다음과 같은 설정 완료창이 뜨는 것을 알 수 있습니다.

 

 

solana config 파일 저장 위치와, keypair 위치가 뜹니다. 제 솔라나 월렛 키페어가 만들어져있기 때문에 뜨는 것이죠. 만약 keypair가 뜨지 않는다면, 생성해주셔야 합니다.

 

 

 

  solana-keygen new

 

솔라나 키를 이 명령어로 생성해줄 수 있습니다.

 

3.2) 스마트 컨트랙트

솔라나는 러스트를 스마트 컨트랙트 언어로 사용한다고 했습니다. 러스트 기본 언어 문법은 배워두는게 좋습니다. 모른다고 하더라도 잠시 작성할 수는 있겠지만, 어쨌든 기초적인 것들은 알아두는게 좋으니까요.

 

solana-helloworld 프로젝트를 살펴보시면 program-rust/src/lib.rs 파일이 존재합니다. 이게 러스트 파일인데요, 이 파일이 컴파일되면 솔라나 스마트 컨트랙트가 됩니다. 한번 해당 파일을 살펴봅시다.

 

 

 

  use borsh::{BorshDeserialize, BorshSerialize};
  use solana_program::{
  account_info::{next_account_info, AccountInfo},
  entrypoint,
  entrypoint::ProgramResult,
  msg,
  program_error::ProgramError,
  pubkey::Pubkey,
  };
   
  /// Define the type of state stored in accounts
  #[derive(BorshSerialize, BorshDeserialize, Debug)]
  pub struct GreetingAccount {
  /// number of greetings
  pub counter: u32,
  }
   
  // Declare and export the program's entrypoint
  entrypoint!(process_instruction);
   
  // Program entrypoint's implementation
  pub fn process_instruction(
  program_id: &Pubkey, // Public key of the account the hello world program was loaded into
  accounts: &[AccountInfo], // The account to say hello to
  instruction_data: &[u8], // Ignored, all helloworld instructions are hellos
  ) -> ProgramResult {
  msg!("Hello World Rust program entrypoint");
   
  // Iterating accounts is safer then indexing
  let accounts_iter = &mut accounts.iter();
   
  // Get the account to say hello to
  let account = next_account_info(accounts_iter)?;
   
  // The account must be owned by the program in order to modify its data
  if account.owner != program_id {
  msg!("Greeted account does not have the correct program id");
  return Err(ProgramError::IncorrectProgramId);
  }
   
  // Increment and store the number of times the account has been greeted
  let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
  let num: u32 = instruction_data[0] as u32;
  greeting_account.counter += num;
  greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
   
  msg!("Greeted {} time(s)!", greeting_account.counter);
   
  Ok(())
  }
   
  // Sanity tests
  #[cfg(test)]
  mod test {
  use super::*;
  use solana_program::clock::Epoch;
  use std::mem;
   
  #[test]
  fn test_sanity() {
  let program_id = Pubkey::default();
  let key = Pubkey::default();
  let mut lamports = 0;
  let mut data = vec![0; mem::size_of::<u32>()];
  let owner = Pubkey::default();
  let account = AccountInfo::new(
  &key,
  false,
  true,
  &mut lamports,
  &mut data,
  &owner,
  false,
  Epoch::default(),
  );
  let instruction_data: Vec<u8> = Vec::new();
   
  let accounts = vec![account];
   
  assert_eq!(
  GreetingAccount::try_from_slice(&accounts[0].data.borrow())
  .unwrap()
  .counter,
  0
  );
  process_instruction(&program_id, &accounts, &instruction_data).unwrap();
  assert_eq!(
  GreetingAccount::try_from_slice(&accounts[0].data.borrow())
  .unwrap()
  .counter,
  1
  );
  process_instruction(&program_id, &accounts, &instruction_data).unwrap();
  assert_eq!(
  GreetingAccount::try_from_slice(&accounts[0].data.borrow())
  .unwrap()
  .counter,
  2
  );
  }
  }
view rawlib.rs hosted with ❤ by GitHub

 

조금 긴데 사실 간단합니다. 지금 단계에서 알아야 할 내용은 얼마 없거든요.

 

borsh는 Binary Object Representation Serializer for Hashing 의 약어입니다. 바이너리로 풀어진 오브젝트를 시리얼라이징하거나 디시리얼라이징할 때 쓰이는 일종의 라이브러리라고 생각하시면 됩니다. node에도 동일하게 존재하는 라이브러리입니다.

 

entrypoint!() 에서 실제 스마트 컨트랙트가 선언되는데, 이 안 내용을 한번 살펴보시죠.

 

account.iter()는 map과 비슷한겁니다. account를 iterable하게 돌려서 account를 하나씩 살펴본다는 의미입니다. 그래서 next_account_info로 account를 가지고 온 후, 해당 account_info에 데이터를 set해줄 준비를 할 겁니다. 데이터를 저장해야 그 데이터로 무언가 로직을 돌릴테니, 이번 예제에선 데이터를 저장하는 로직만 해볼거에요. 그러니까... UPDATE, INSERT, SELECT 세 기능을 구현해 볼 겁니다.

 

 

  let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
  let num: u32 = instruction_data[0] as u32;
  greeting_account.counter += num;
  greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
   
  msg!("Greeted {} time(s)!", greeting_account.counter);
   
  Ok(())

 

여기가 핵심 로직인데요, account의 data를 가지고 와서, instruction_data를 해당 data에 set 해주는 부분입니다. instruction_data는 우리가 직접 넣어주는 데이터가 될 거에요. 그 다음 다시 저장해주고 Ok() 해주면, 해당 노드 블럭에 스마트 컨트랙트 내용이 저장됩니다.

 

여기까지 간단하게 러스트 코드를 살펴봤습니다. 너무 간단해서 볼 내용도 별로 없죠?

 

3.3) 데이터 set 하고 불러오기

부차적인 내용은 다 빼고, 하나하나 차근차근 빠르게 보겠습니다.

 

1] 커넥션 맺기

 

  const rpcUrl = await getRpcUrl();
  connection = new Connection(rpcUrl, 'confirmed');
  const version = await connection.getVersion();
  console.log('Connection to cluster established:', rpcUrl, version);

 

rpcUrl은 devNet 주소입니다. jsonRPC로 통신할 url이 어딘지 fetch해오거나 아니면 선언해주는 곳입니다. 그 이후, confirmed 블록만 가져와서 커넥션을 맺어주는걸 new Connection을 통해 진행합니다.

 

 

 

  let fees = 0;
  if (!payer) {
  const {feeCalculator} = await connection.getRecentBlockhash();
   
  // Calculate the cost to fund the greeter account
  fees += await connection.getMinimumBalanceForRentExemption(GREETING_SIZE);
   
  // Calculate the cost of sending transactions
  fees += feeCalculator.lamportsPerSignature * 100; // wag
   
  payer = await getPayer();
  }
   
  let lamports = await connection.getBalance(payer.publicKey);
  if (lamports < fees) {
  // If current balance is not enough to pay for fees, request an airdrop
  const sig = await connection.requestAirdrop(
  payer.publicKey,
  fees - lamports,
  );
  await connection.confirmTransaction(sig);
  lamports = await connection.getBalance(payer.publicKey);
  }
   
  console.log(
  'Using account',
  payer.publicKey.toBase58(),
  'containing',
  lamports / LAMPORTS_PER_SOL,
  'SOL to pay for fees',
  );
view rawgetPayer.ts hosted with ❤ by GitHub

 

payer를 정해줍니다. 이번엔 devnet이기 때문에, payer는 우리죠. 따라서 만약 부족하면 데브넷에서 에어드롭을 받는 로직까지 추가합니다. 실제 디앱에선 이렇게 하면 안되고 에어드롭 받는 부분은 제외해야합니다. 만약 잔액이 부족하면 그냥 에러를 뱉어야죠.

 

 

 

  try {
  const programKeypair = await createKeypairFromFile(PROGRAM_KEYPAIR_PATH);
  programId = programKeypair.publicKey;
  } catch (err) {
  const errMsg = (err as Error).message;
  throw new Error(
  `Failed to read program keypair at '${PROGRAM_KEYPAIR_PATH}' due to error: ${errMsg}. Program may need to be deployed with \`solana program deploy dist/program/helloworld.so\``,
  );
  }

 

프로그램 에러가 있는지, 프로그램이 있긴 한지 체크해봅니다.

 

 

 

  const instruction = new TransactionInstruction({
  keys: [{pubkey: greetedPubkey, isSigner: false, isWritable: true}],
  programId,
  data: Buffer.alloc(1, 2), // 실제 넣을 데이터
  });
   
  await sendAndConfirmTransaction(
  connection,
  new Transaction().add(instruction),
  [payer],
  );
view rawtransaction.ts hosted with ❤ by GitHub

 

이제 실제로 트랜잭션을 발생시킵니다. TransactionInstruction의 input이 아까 러스트랑 동일하죠? 맞습니다, 러스트에서 받는 인자들입니다. 따라서 data에 저렇게 넣으면 2라는 숫자가 들어갑니다. 중요한건, Buffer로 선언된 ㄹㅇ 바이너리 숫자가 들어간다는 사실입니다.

 

 

 

  const accountInfo = await connection.getAccountInfo(greetedPubkey);
  if (accountInfo === null) {
  throw 'Error: cannot find the greeted account';
  }
  const greeting = borsh.deserialize(
  GreetingSchema,
  GreetingAccount,
  accountInfo.data,
  );

 

트랜잭션이 맺어지고 난 이후 현재 데이터는 어떻게 불러올까요? getAccountInfo를 통해 가져와서, data를 불러옵니다. 그러면 우리가 set한 데이터 정보들이 나옵니다!

 

 

4. 마무리

이렇게 솔라나로 아주 간단한 스마트 컨트랙트를 만들어 봤습니다. 노드를 통해 통신할 수 있으니, 아마 노드 웹서버를 통해 통신할 수도 있겠지요.

 

이런 스마트 컨트랙트를 openDB, open Logic으로도 사용할 수 있습니다. 누구나 볼 수 있는 DB, 그리고 누구나 볼 수 있는 로직이기 때문에 절대로 속일 수 없다는 특징을 가지게 된 것이죠. 누구나 검증할 수 있게 된 것입니다. 물론, 스마트컨트랙트를 누구나 발생시킬 수 있기 때문에, 실제 구현상에선 greetedPubKey의 SEED를 적절하게 잘 숨겨서 보관하는 것도 중요합니다. 해당 seed가 곧 DB의 암호가 되는 셈이니까요.

 

이 글이 한국어 사용자의 스마트 컨트랙트 이해도 향상에 조금이나마 도움이 되었으면 좋겠습니다! 기회가 된다면 다음 글로 곧 스마트컨트랙트의 실제 활용도 한번 보여드리겠습니다.

 

추가)

사실 프로그램을 만들 때엔 anchor를 이용하시는 것이 좋습니다. 해당 글도 읽어보세요!

https://bakjuna.tistory.com/138

 

[anchor] anchor를 이용하여 solana program (smart contract) 작성하기

1. 들어가며 anchor는 solana 프로그램을 작성할 때 훨씬 더 쉽게 작성할 수 있게 도움을 주는 라이브러리입니다. 솔라나 프로그램을 네이티브로 작성하는 것보다, anchor를 이용하여 작성하는게 훨씬

bakjuna.tistory.com

 

블로그 이미지

wtdsoul

,