Categories
Ref: medium

[솔라나 NFT 개발] 1편: 솔라나 기본 개념 이해하기 — 컨트랙트와 스토리지의 구조

여기서 Instruction이란 프로그램이 실행하는 Logic의 최소 단위입니다. Transaction에는 Instruction, Signature, 계정 주소 등을 담고 있으며, Solana Runtime은 트랜잭션을 검증하고, 각 Instruction들을 순서대로, atomic하게 처리(processing)합니다. 즉, 하나의 Transaction이 복수의 Instruction을 담고 실행되기 때문에, 이 점에서 이더리움과의 차이가 있습니다. 각 Instruction들은 적절한 Program들에 배정되어 Program이 해당 Instruction들을 수행합니다.

Program 내부에서 Instruction이 처리되는 과정은 위 도표를 참고하면 좋습니다. Program에 입력된 Instruction은 Deserialize 과정을 거칩니다. 솔라나에서 계정 간의 메세지 통신은 byte stream으로 이루어집니다. 따라서 Program은 byte stream으로 입력받은 내용을 사람이 읽을 수 있는 형태로 deserialize하는 과정을 거치며, 반대로 Program으로부터 출력할 때는 다시 byte stream으로 변환하는 serialize 과정을 거칩니다.

해당 Instruction에 담긴 내용에 따라 프로그램 내부 함수들이 호출, 수행됩니다. 또한 Instruction에는 계정 주소와 Signature 등도 담겨져 있는데, Instruction 처리 과정에서 필요한 Data Account들을 불러와, 상태를 변경하게 됩니다.

프로그램은 인스트럭션을 받은 후, 내부에서 실행하기 위해 역직렬화 과정을 거치며, 다시 직렬화 과정을 거쳐 인스트럭션을 생성합니다.
[그림 1–6] Instruction의 serialization & deserialization 출처: 솔라나 쿡북
💡 DSRV’s Tip: Serde에 대해 알아보기

위 설명에 대한 코드 수준의 이해는 솔라나 쿡북을 참고하면 좋습니다. [9]
짧게 Serialization 과 Deserialization 에 대해 설명해보도록 하겠습니다.

전반적인 프로세스는 위 도표(그림 1-6)와 같은데, 클라이언트 ↔ 프로그램 또는 프로그램 ↔ 프로그램 간의 RPC 통신은 Serialized 된 byte stream으로 주고 받습니다. 프로그램은 받은 byte의 flag에 따라 Instruction의 종류를 분류하고, Payload를 Deserialize하는 과정을 거칩니다.

Borsh Deserialize 과정 살펴보기위와 같은 코드를 실행하면, 다음과 같은 결과가 출력됩니다. Primitive(255, 65535, 4294967295, "hello", "world", [1, 2, 3, 4, 5], {"cookbook": "recipe", "recipe": "ingredient"}) 이는 byte array인 prim을 Primitive struct로 deserialize하는 과정의 예시입니다.  어떻게 대응되는 지는 아래 도표를 참고해주세요.
Borsh Deserialization 모식도
[그림 1–7] Borsh Deserialization 예시, 출처 @c0wjay

이더리움에서는 ERC20 규격 등의 스마트 컨트랙트를 이용하여 새로운 토큰(Token)을 발행하는 것이 가능합니다. 반면, 솔라나에서는 토큰을 발행하는 프로그램(스마트 컨트랙트)은 1개만 존재하며(SPL Token Program), 해당 프로그램에 create-token이라는 instruction을 보내어 새로운 토큰을 발행합니다.

Mint Account 관계도
[그림 1–8] Mint Account, 출처: Twitter, @pencilflip

새로운 토큰 발행을 위해, Token Program에 create-token instruction을 보내면 어떤 일이 일어날까요?Token Program은 Mint Account 또는 Token Mint 라 불리는 Data account를 생성합니다. 각 타입의 토큰은 모두 어떤 한 종류의 Mint Account 과 관련되어 있으며, Mint Account는 토큰에 대한 metadata를 저장하고 있습니다 해당 토큰의 Mint Authority(발행 권한)는 User Account가 가지고 있습니다. 단, owner는 프로그램의 쓰기 권한이므로 Mint Authority와는 다릅니다. 이는 Mint Authority를 가진 User Account만 해당 Token의 발행이 가능하다는 뜻입니다.

Token Account 관계도
[그림 1–9] Token Account, 출처: Twitter, @pencilflip

또한 User는 Token Program으로 하여금, Token Account를 생성할 수 있습니다. Token Account는 특정 토큰에 대한 특정 User의 지갑과 같은 역할을 하는 Data Account로, 해당 토큰의 갯수를 저장합니다.

[그림 1–10] 여러 유저들의 Token 구조, 출처: Twitter, @pencilflip

토큰의 전송은 Token Program에 transfer instruction을 보내면 가능합니다. 이 때, 받는 사람도 해당 토큰에 대한 Token Account를 가지고 있어야 합니다. (이것에 대한 설명은 추후에 다른 컨텐츠로 다루도록 하겠습니다.)

만약 받는 사람이 해당 토큰에 대한 Token Account를 가지고 있지 않다면, 문제가 생길 수 있습니다. 특히 받는 사람이 off-line일 경우엔, 받는 사람이 계정을 만들 때까지 보내는 사람이 기다리기가 어려울 수 있기 때문입니다.

이를 위해 솔라나에서는 Associated Token Account (ATA) Program이 있습니다. 이 프로그램은 받는 사람이 해당 토큰에 대한 Token Account가 없더라도, 보내는 사람이 받는 사람에 대한 Token Account를 미리 만들어서 그 곳으로 토큰을 전송할 수 있도록 토큰 계정을 미리 만들어주는 기능을 합니다. [10] 이는 해당 프로그램이 Mint Address와 User Address로부터 Associated Token Address를 해시 함수 등을 사용해서 결정론적으로 도출해내기 때문에 가능해집니다.

용어 정리: PDA란?본 글에서 PDA는 Program-Derived Address를 칭하고, 해당 Program-Derived Address를 주소로 하는 계정을 PDA Account라 칭합니다.

프로그램은 Cross-Program Invocation(CPI)을 통해 다른 프로그램을 호출할 수 있습니다. [11] 솔라나 프로그램 코드에서는 invoke 함수를 흔히 보실 수 있는데요. 이 메소드를 활용하면 다른 프로그램의 함수를 실행할 수 있습니다. 또한 Invocation은 호출한 프로그램의 추가 Invocation을 만들 수 있지만, 호출 횟수(call depth)는 4회로 제한됩니다. 이를 통해 재진입 공격(Re-entrancy Attack)을 방지합니다.

💡 DSRV’s Tip: 재진입 공격(Re-entrancy Attack)에 대해 알아보기재진입 공격(Re-entrancy Attack)이란, 컨트랙트 A가 외부 컨트랙트 B를 호출하는 구조에서,
컨트랙트 A의 실행이 완료되기 전에, 컨트랙트 B에서 다시 컨트랙트 A를 재귀적으로 호출하여 컨트랙트 A의 금액을 모두 인출하는 공격입니다.
예를 들자면, 아래와 같은 로직으로 공격이 수행됩니다. ([참고 예제](<https://solidity-by-example.org/hacks/re-entrancy>))1. 컨트랙트 A 에 해커의 돈이 100원이 있고, 이를 돌려받고자 한다.
2. 돌려받는 주소를 해커가 작성한 컨트랙트 B로 한다.
3. 컨트랙트 B에는 fallback function(돈이 입금되면 실행되는 함수)이 있고, 이 함수는 다시 컨트랙트 A를 호출하여 돈을 돌려받는 과정을 재실행한다.
4. 아직 컨트랙트 A의 실행은 완료되지 않았으므로(즉 컨트랙트 A에는 해커의 돈이 100원 그대로 있다고 상태가 저장되어 있으므로), 다시 100원의 송금이 실행된다.
5. 이는 다시 재귀 호출을 만들어, 계속해서 컨트랙트 A에서 B로 100원씩 송금이 실행된다.

만약 PDA Account가 다른 Program의 함수를 실행할 때, 별도의 Signature 가 필요하다면 어떻게 처리해야 할까요? 이럴 때에는invoke_signed 함수를 사용할 수 있습니다.

이는 Program으로 하여금 PDA라 하는 공개키를 Signature 처럼 사용할 수 있도록 합니다. 다른 외부 유저에서는 사용이 불가합니다. 이를 활용하여 Instruction을 만들어서 자산을 전송하는 것과 같은 행동을 할 수 있도록 합니다.

PDA는 Program ID와 Seeds를 해시 함수(sha256)에 넣어 생성됩니다. 정확히는 생성하는 것이 아니라, 위 함수에 넣어 ed25519 타원 곡선 위에 있는 공개 키 값을 찾는 것입니다. 그 값은 Program이 존재하기 전부터 이미 해시 함수 위에 Input에 대응되는 값으로 있기 때문입니다. [12] 실제로 solana.js 등에 있는 create_program_addressfind_program_address 함수는 같은 기능을 합니다.

PDA에 대응되는 Data Account를 만들 수 있으며, 이 때 Account의 주소는 PDA입니다. 이 때 PDA Account는 두 가지 기능을 갖습니다.

  1. 프로그램의 상태를 저장한다.
  2. Cross-Program Invocation에 서명(sign)한다.

create_program_address로 생성된 Program 주소는 지갑의 공개 키와 구분할 수 없습니다. 따라서 Program은 런타임에게 프로그램 주소 생성에 사용된 Seed 구문을 제공하고, 런타임은 해당 Program ID와 제공받은 Seed 구문을 ed25519 알고리즘을 통해 검증합니다.

PDA를 통해 토큰 B에 대한 Account(Token Account)를 만든다면, 이 Account에 대한 소유권은 PDA가 갖고, PDA에 대한 소유권은 해당 프로그램이 갖습니다. 즉, PDA는 일종의 해당 프로그램만 사용 가능한 Account의 공개키이자 서명(Signature)인 것입니다.

💡 DSRV’s Tip: 왜 Program-Derived Address(PDA)가 필요하나요?

아래와 같은 중고거래 예시를 가정하여 설명해보겠습니다.

"밥이 앨리스한테 물건을 보내고, 돈을 받는다"

그렇다면 밥 입장에서는 물건을 보내기 전에, 앨리스가 돈을 주겠다는 확약을 받아야하고, 앨리스는 정상적인 물건인지 확인 전까지는 돈을 보내주기가 꺼려집니다.
이 때 3자에게 돈을 맡겨서 물건의 배송이 완료된 후에, 돈을 보내는 구조로 처리하는 것이 일반적입니다.

온라인 상의 거래도 항상 양쪽이 모두 온라인인 상황에서만 거래되는 것이 아니라, 한쪽이 오프라인이어도 거래가 진행되는 상황이 있을 수 있습니다. 이 때 제 3자가 돈을 보관해야하는데, 솔라나에선 프로그램이 그 역할을 담당합니다. 문제는 그 프로그램이 특정 유저에 의해 작동이 된다면, 매 거래시마다 유저가 서명해야 한다는 문제가 생기고, 또한 그 유저가 나쁜 마음을 먹게 된다면 악의적으로 거래가 이루어질 수 있습니다. 예를 들면, 위에서 앨리스에게 뒷돈을 받아 밥에게 송금을 취소하는 경우가 있을 것입니다.

그렇다고 프로그램이 프로그램 소유의 개인키를 갖고 있기에는, 오픈소스 코드라면 개인키가 공개되거나 탈취될 수 있다는 문제점을 갖고 있습니다. 이러한 문제인식에서 PDA라는 개념이 나오게 되었고, 개인키가 존재하지 않으면서도 “프로그램”만 사용할 수 있고, 이를 상호 검증할 수 있는 서명(Signature)의 개념이 생긴 것입니다.

[그림 1–10] 여러 유저들의 Token 구조, 출처: Twitter, @pencilflip

솔라나에서 지갑(Wallet)은 Keypair들의 모음입니다. 위 그림 [1–10]에서의 User Account의 주소가 공개키가 되며, 해당 계정에는 Lamports를 저장합니다. Token Account는 특정 Token의 주소와 유저 지갑의 공개키를 입력하여 생성 가능하며, 지갑의 UI는 공개키에 대응되는 Token Account들의 잔고를 조회 및 합하여 총계를 보여줍니다.

Solana에서 NFT의 개념이란, Mint Account에 연결된 ATA에 토큰을 단 한 개만 발급한 후, 추후 발행을 금지시키는 형태로 구현합니다. [13]

메타데이터의 처리는 메타플렉스(Metaplex)에 의해 구현된 token-metadata-program에 의해 작동됩니다. [14] 메타플렉스(Metaplex)는 스스로 Metadata Program을 Token Mint의 Decorator라 칭합니다. Metadata Program은 해당 Mint Account의 Key를 Seed에 포함시켜 만든 Metadata PDA account를 만듭니다.

온체인 상에 저장되는 Metadata는 하기와 같습니다.

여기서 URI는 오프체인(off-chain) JSON metadata를 가리키는 포인터입니다(예시). NFT에 대한 attributes 등 세부 특성에 대한 metadata는 보통 URI를 통한 오프체인(Arweave 등)에 저장됩니다.

  1. Arweave에 이미지를 업로드합니다.
  2. 이미지 주소가 담긴 json 형식의 metadata를 다시 Arweave에 업로드합니다.
  3. 해당 Arweave URI가 포함된 NFT를 발행합니다.
  4. 발행 후 생성된 Mint Account 주소를 통해 NFT metadata를 조회 가능합니다.
NFT 어카운트들의 관계도
[그림 1–11] NFT account들의 구조, 출처: Twitter, @pencilflip

보다 상세한 내용은 솔라나 쿡북에서 확인하실 수 있습니다. [15] 하나의 NFT를 위해 솔라나에서 어떤 Account들이 구성되어 있는지 자세히 알아보고 싶다면, pencilflip의 트위터 글을 참고하면 좋습니다. (참고: Sigrid Jin님의 트위터 번역본)

이번 시간에는 솔라나에서 Program AccountData Account 가 각각 어떻게 구현되어 있는지 알아봤으며, 트랜잭션의 구성과 Program이 어떻게 실행되는지 알아보았습니다. 그 과정에서 가장 중요한 개념인 PDA와 CPI에 대해 다루었으며, 마지막으로는 솔라나에서 NFT가 어떻게 구현되어 있는 지 다루어 보았습니다.

다음 시간에는 메타플렉스(Metaplex)의 캔디머신이라는 일종의 SaaS 툴을 이용하여 솔라나에서 NFT를 민팅하는 과정을 high level에서 다룹니다. 이 글이 솔라나에서 NFT 프로젝트를 시작하고자 하는 많은 개발자분들에게 도움이 되었기를 바라며, 다음 글로 찾아오도록 하겠습니다. 감사합니다.



Source link

About Author

Leave a Reply

Your email address will not be published. Required fields are marked *