본문 바로가기
암호/블록체인

블록체인 7-1

by 주황딱지 2024. 12. 6.

Solidity introduction

 

정의


Solidity는 이더리움(Ethereum) 블록체인을 위한 스마트 계약을 작성하기 위한 고수준 언어(high-level language)이다.

 

Contracts의 특징:

  • 스마트 contract는 전통적인 객체 지향 프로그래밍 언어(Java 등)의 클래스와 유사한 캡슐화된 단위(encapsulated units)로 정의된다.
  • 각 contract는 지속적인 상태(persistent state)를 가지며, 이는 컨트랙트 내에서 상태 변수(state variables)로 정의된다.

Functions: 계약의 상태를 변경하거나 다른 계산을 수행하기 위해 함수(functions)를 사용한다.
Solidity 컴파일 및 배포: Solidity 코드는 바이트코드(bytecode)로 컴파일되며, 블록체인에 배포되면 영구적(persistent)이고 불변(immutable) 상태를 유지한다.
한 번 배포된 스마트 계약은 수정하거나 패치(patch) 배포가 불가능하다.
따라서 스마트 계약은 배포 전에 반드시 완벽히 테스트되어야 한다.

 

소스코드

 

스마트 컨트랙트 소스 코드: 스마트 컨트랙트의 소스 코드는 블록체인에 저장되지 않는다. 대신, 블록체인에는 바이트코드(bytecode)만 저장된다.

 

바이트코드는 EVM(Ethereum Virtual Machine)에서 실행될 연속적인 명령어(opcodes)를 나타낸다.

추가적인 분석이 없으면 바이트코드만으로는 스마트 계약의 구체적인 목적을 명확히 이해하기 어렵다.

 

스마트 컨트랙트의 소스 코드는 공개될 수 있으며, 이를 통해 사용자들이 계약의 목적과 기능을 검증할 수 있다.
Etherscan.io: Etherscan.io는 가장 널리 사용되는 이더리움 블록 탐색기로, 스마트 계약의 소스 코드와 바이트코드의 검증을 지원한다.

 

From Solidity Source Code to a Deployed Smart Contract(소스코드를 스마트 컨트랙트로 전환)

 

과정 설명
1. Contract Development (컨트랙트 개발):
Solidity 프로그래밍 언어를 사용하여 스마트 컨트랙트 코드(.sol 파일)를 작성한다.
이 단계는 개발자의 개인 시스템에서 이루어지며, 일반 대중은 이 과정에 접근할 수 없다.

 

2. Compilation (컴파일):
Solidity 컴파일러가 .sol 파일을 읽고 이를 EVM 바이트코드(EVM bytecode)로 변환한다.
변환된 바이트코드는 Ethereum Virtual Machine에서 실행될 준비가 된다.
이 단계 역시 개인적인 환경에서 실행된다.

 

3. Transaction Creation (트랜잭션 생성):
컴파일된 바이트코드는 16진수(hex) 형식으로 인코딩되고, 네트워크에 트랜잭션으로 전송된다.
이 시점부터 블록체인 네트워크에 기록되며, 공개적으로 접근이 가능해진다.

 

4.Transaction Mined (트랜잭션 채굴):
네트워크에 제출된 바이트코드는 블록에 포함되어 채굴된다.
컨트랙트가 블록체인에 포함되면, 스마트 컨트랙트가 활성화되어 사용 가능해진다.

 

  • 개발 단계(1번과 2번)는 개인 컴퓨터에서 비공개로 수행된다.
    대중은 이 코드에 접근할 수 없다.
  • 3번, 4번은 트랜잭션이 블록체인에 기록되면서 공개적으로 확인 가능해진다.
    블록체인에 저장된 바이트코드는 누구나 접근할 수 있다.

Solidity Smart contract file 구성

 
State Variables (상태 변수):
상태 변수는 스마트 계약의 스토리지에 영구적으로 저장된다.
상태를 변경하려면 트랜잭션이 필요하며, 이는 이더(ether) 비용이 발생한다.
읽기 작업은 무료이며 트랜잭션이 필요없다.


struct (구조체):
Solidity에서 구조체(struct)는 데이터 모델을 정의하는 데 사용된다.
여기서는 Tutor라는 구조체가 정의되었으며, firstName과 lastName이라는 두 개의 문자열 필드를 포함한다.
mapping (매핑):
매핑은 Solidity에서 키-값 쌍을 저장하는 자료구조이다.
예제에서는 address를 키로 하고 Tutor 구조체를 값으로 하는 tutors라는 매핑이 있다.

 contractBBSE {
     struct Tutor {
         string firstName; 
         string lastName;
     }
 mapping(address=> Tutor) tutors;
 address professor;

 

Function Modifiers
코드 재사용을 간편하게 해주는 도구로, 함수의 동작을 변경하거나 확장하는 데 사용된다.
함수 실행 이전 및/또는 이후에 코드를 실행할 수 있다.
주요 특징:

  • 동작 변경: 함수 실행의 흐름을 수정하거나 특정 조건을 추가한다.
  • 코드 위치 지정: _ (밑줄)는 원래 함수 코드가 삽입될 위치를 나타낸다.
  • onlyProfessor는 함수 호출자가 계약의 professor인지 확인한다.
 modifieronlyProfessor{
    require(msg.sender == professor);
    _;
 }

 

Constructor(생성자)
Contract가 배포될 때 단 한 번 실행되는 특별한 함수이다.
Constructor는 Contract 생성(transaction) 중에 실행되며, 이후 다시 호출될 수 없다.
주요 특징:

  • 단일 실행: Contract 생성 시 딱 한 번 호출되며, 생성 이후에는 호출할 수 없다.
  • 초기화 역할: Contract의 초기 상태를 설정하는 데 사용된다.
    예: 소유자(Owner) 주소 지정, 초기 변수 값 설정.
  • Gas 비용: Constructor 실행에는 Gas가 소모된다.
    더 복잡한 Constructor는 더 높은 배포 비용을 발생시킬 수 있다.
  • 여기서는 professor 변수를 컨트랙트를 배포한 msg.sender로 설정한다.
 constructor() public {
 professor = msg.sender;
 }

 

Function

Functions의 역할:

  • 상태 변경: 컨트랙트의 상태를 변경하는 데 사용된다.
    예: 새로운 데이터를 저장하거나, 기존 데이터를 수정.
  • 상태 읽기: 컨트랙트의 상태를 조회하는 데 사용된다.
    예: 특정 값 반환, 데이터 읽기.
  • 구성 요소:
    이름(Name): 함수의 고유 이름.
    서명(Signature): 함수 이름과 파라미터 목록의 조합.
    가시성(Visibility): 함수 접근 수준을 정의 (public, private 등).
    타입(Type): 함수가 실행 중 상태를 변경하는지 여부 정의 (view, pure 등).
    수정자(Modifiers): 추가적인 로직을 제공하는 데 사용 (예: 접근 제어).
    반환 값(Return Type): 함수가 반환하는 값의 타입.

함수 정의 형식:
 func%on (<parameter type>) 

{internal|external|public|private}

[pure|constant|view|payable]

[(modifiers)]

[returns (<return types>)]

 functiongetProfessor() view returns (address) {
     return professor;
 }
 
 // This funcEon adds a new tutor
 function addTutor(address tutorAddress,
 string firstName, string lastName) onlyProfessor {
     Tutor tutor = tutors[tutorAddress]; 
     tutor.firstName= firstName;
     tutor.lastName = lastName;
 }

getProfessor: 컨트랙트의 professor 주소를 반환하는 읽기 함수.
addTutor: 새로운 튜터를 추가하는 함수로, 이름과 주소를 입력받아 매핑에 저장한다.

 


언어 특성

 

Solidity는 JavaScript 영감을 받은 언어이다.
유사한 문법: Solidity는 JavaScript를 기반으로 하지만 객체 지향 프로그래밍의 고급 기능들을 제공한다.
정적 타입 사용: JavaScript의 동적 타입과 달리, Solidity는 정적 타입을 사용하여 더 엄격한 코드 구조를 제공한다.

 

내장 데이터 타입

  • 기본 타입:
    int, uint: 부호가 있는/없는 정수형.
    bool: 논리형(true 또는 false).
    address: 이더리움 주소를 표현.
  • 복합 타입:
    array: 고정 크기 또는 동적 배열.
    struct: 여러 변수를 그룹화하는 사용자 정의 타입.
    enum: 상수값을 정의하기 위한 열거형.
    mapping: 키-값 형태의 데이터 저장소(빠른 조회 가능).

내장 객체
block: 현재 블록에 대한 정보 포함(예: 시간, 난이도).
msg: 계약에 보내진 메시지 정보(예: 송신자 주소, 전송 값).
tx: 트랜잭션 수준의 데이터 포함(예: 가스 가격).

 

내장 함수

  • 에러 처리:
    assert(): 내부 오류 및 불변 조건 확인.
    require(): 입력값 및 조건 검증.
    revert(): 트랜잭션을 명시적으로 되돌림.
  • 수학 및 암호화:
    addmod(), mulmod(): 모듈러 덧셈 및 곱셈.
    sha3(), keccak256(), sha256(), ripemd160(): 암호화 해시 함수.
    ecrecover(): 서명에서 공개 키를 복구.
  • 블록체인 정보:
    gasleft(): 남은 가스 반환.
    blockhash(): 특정 블록의 해시를 가져옴.
  • 계약 관련:
    selfdestruct(): 계약을 삭제하고 잔액을 지정된 주소로 전송.

문자열 세트

  • 이더리움 전용 문자열:
    예: int a = 5 eth는 이더 단위 값을 할당.

제어 흐름

  • 조건문: if, else, ? : (삼항 연산자).
  • 반복문: for, while, do.
  • 종료 지점: break, continue, return.

Solidity에서 함수와 변수의 가시성 (Function and Variable Visibility)

  1. External (외부)
    다른 컨트랙트 또는 특정 지갑 트랜잭션을 통해 호출 가능.
    컨트랙트 내부에서는 직접 호출할 수 없음.
    항상 공개적으로 보이는 함수로 정의됨.
  2. Public (공개)
    내부 및 외부 모두에서 호출 가능:
    내부: 컨트랙트 자체에서 호출.
    외부: 다른 컨트랙트 또는 트랜잭션을 통해 호출.
    Public으로 정의된 상태 변수는 컴파일러에 의해 자동으로 getter 메서드가 생성됨.
  3. Internal (내부)
    같은 컨트랙트 내에서 호출되거나, 해당 컨트랙트를 상속받은 파생 컨트랙트에서만 접근 가능.
    다른 컨트랙트나 트랜잭션을 통해서는 호출할 수 없음.
  4. Private (비공개)
    오직 내부적으로 자신이 소유한 컨트랙트 내에서만 호출 가능.
    파생 컨트랙트에서도 호출 불가.
    가장 제한적인 접근 제어.

EVM 데이터 저장방식


EVM(Ethereum Virtual Machine)은 데이터를 Storage, Memory, Stack의 세 가지 영역에 저장할 수 있다.

  1. Storage
    컴퓨터의 하드 드라이브에 비유됨.
    스마트 컨트랙트마다 데이터를 영구적으로(persistent) 저장.
    함수 호출 사이에서도 데이터를 유지하며, 이후 실행 시에도 저장된 데이터에 접근 가능.
    활용: State Variables(상태 변수)는 기본적으로 Storage에 저장.
  2. Memory
    컴퓨터의 RAM에 비유됨.
    임시 저장소로 사용되며, 함수 실행이 끝나면 초기화됨.
    활용: Value-type 로컬 변수 및 함수 매개변수(int, bool, address 등)는 Memory에 저장.
  3. Stack
    EVM은 스택 머신으로 동작하며, 모든 계산은 스택에서 수행.
    최대 1024개의 요소를 포함하며, 각 요소는 256비트 크기를 가짐.
    활용: 모든 연산과 데이터 처리는 스택을 통해 이루어짐.

변수 저장 규칙 (Variables)
1. State Variables (상태 변수): 기본적으로 Storage에 저장.
2. Value-type 로컬 변수와 함수 매개변수: Memory에 저장.
3. Reference-type 로컬 변수 (배열, 구조체 등): Memory 또는 Storage에 저장 가능.
4. Reference-type 함수 매개변수: Memory 또는 Calldata(읽기 전용)로 저장 가능.

 

참고:

  • Storage 읽기/쓰기 비용: Storage에서 데이터를 읽거나 수정하는 것은 비용이 많이 듬(Gas 소모).
    권장사항: 반드시 필요한 데이터만 Storage에 저장하고, 가능하면 Memory를 사용하여 중간 계산을 수행한 후 결과만 Storage에 저장.
  • Memory 비용: Storage에 비해 Memory는 비교적 저렴한 Gas 소모.

 


FUNCTION

 

Solidity는 특별한 함수 타입을 제공한다.

 

1. View 함수
블록체인의 상태(state)를 변경하지 않는 함수.
상태 변수(state variables) 값을 읽을 수 있음.
블록체인 데이터에 읽기 전용 접근을 제공.

uint state = 5;
function add(uint a, uint b) public view returns (uint sum) { return a + b + state }

 

2. Pure 함수
상태를 읽거나 변경하지 않는 함수.
상태 변수는 사용하지 않으며, 함수 자체의 입력과 출력만을 다룸.
수학적 계산이나 내부 로직 구현에 적합.

function add(uint a, uint b) public pure returns (uint sum) { return a +b }

 

3. Fallback 함수
이름이 없는 특별한 함수.
정의된 다른 함수가 호출되지 않을 때 호출됨.
파라미터가 없고 반환값도 없음.
주로 이더를 수신하거나 잘못된 함수 호출을 처리하는 데 사용.

function() { /* … */ }

 

4. Payable 함수
기본적으로 이더(Ether)를 함수에 보낼 수 없으며, 이 시도가 실패(트랜잭션 리버트)하게 된다.
이는 의도적으로 설정된 동작으로, 실수로 보낸 이더가 손실되는 것을 방지하기 위해 설정되었다.
그러나 스마트 계약에서 이더를 수락해야 할 때(예: ICO 참여 등) payable 키워드를 사용한다.

 function buyInICO() public payable { /* … */ }

 

생성자(constructor) 또는 이더를 수신할 수 있는 주소를 정의할 때도 payable이 필요하다.

constructor payable { /* … */ }, function withdraw (address payable _to) public { /* … */ }

 

일반 address를 payable로 변환하려면 명시적 캐스팅( payable (<address>) )이 필요하다.

address public customer;
 function transfer (uint amount) public { 
payable(customer). transfer(amount);
 }

Function Modifiers

 

특정 조건이 참(True)인지 거짓(False)인지 확인한 후 함수가 실행되도록 해야 할 때 사용된다.
반복되는 인증 메커니즘 코드를 작성하면 유지보수와 보안 문제가 발생할 수 있다.
Solidity는 Modifier를 제공하여 재사용 가능한 코드 작성을 지원한다.
함수를 실행하기 전에 특정 조건(예: msg.sender가 계약 소유자인지)을 검사한다.

modifier 키워드로 정의한다.

아래 코드에서 _(underscore)는 함수 본문이 삽입될 위치를 표시한다.

contract owned{ 
  address public owner;
 
constructor() public { 
  owner=msg.sender;
 }
 
 
modifier onlyOwner { 
   require(msg.sender == owner);
   _;
 }
 
function kill() public onlyOwner { 
   selfdestruct(owner);
 }
} 

 

빈칸에 selfdestruct(owner);를 넣으면 아래 코드와 같은 의미를 가지게 된다.

contract owned{ 
 address public owner;

 constructor() public { 
  owner=msg.sender;
 }
 
 function kill() public { 
   require(msg.sender == owner); 
   selfdestruct(owner);
  }
 }

 

Chaining function modifier


함수에 여러 Modifiers를 적용하는 것이 가능하다.
Modifiers는 왼쪽에서 오른쪽으로 순차적으로 처리된다.
복잡한 검증 과정을 구조화하여 코드 가독성을 향상. 재사용성을 극대화할 수 있다.

아래 코드는 유저가 컨트랙트의 주인이고 1337이더리움보다 많은 계좌 밸런스를 가지고 있어야만 kill함수를 호출할 수 있다. 

contract owned{ 
  address public owner;
 
  constructor() public { 
   owner=msg.sender;
  }

  modifier onlyOwner {   
    require(msg.sender == owner);
    _; // Actual function code is injected here
  }
 
  modifier isRich {
    require(msg.sender.balance > 1337 ether);
    _; // Actual funcDon code is injected here
  }
 
  funcDon kill() public onlyOwner isRich { 
   selfdestruct(owner);
  }
}

함수 오버로드

 

Solidity는 오버로드 함수를 허용한다. 즉 다른 서명으로 같은 함수를 2번 이상 정의할 수 있다.

이는 특정 상황에서 적용하는 데 필요하다.

function sendEther(uint amount) { 
  require(this.balance >= amount); 
  payable(msg.sender).transfer(amount);
}

function sendEther(uint amount, address payable to) { 
  require(this.balance >= amount); 
  to.transfer(amount);
}

 

만약 주소 인자(address payable to) 없이 sendEther()를 호출하면 sender에게 Ether가 전송된다(처음 함수). 그렇지 않으면 함수에 매개 변수로 전달된 주소로 전송된다(두 번째 함수).

 

Named function call

 

Solidity는 Named Calls라는 개념을 지원한다.
Named Calls이란 dictionary(키-값 쌍 사용)를 사용해 함수의 매개변수를 명시적으로 전달할 수 있는 방식이다.
기본 동작은 함수 매개변수를 일반적으로 정의된 서명 순서에 따라 전달한다.
그러나 Named Calls를 사용하면 매개변수 순서에 구애받지 않고 명시적으로 값을 지정 가능해진다.

 

코드의 가독성과 유지보수성 향상. 복잡한 함수 호출에서 혼동 방지.

//default function

function myAddFunction(uint a, uint b) returns (uint result) {
 return a+b; 
}

function fourPlusTwo() returns (uint result) {
 return myAddFunction(4, 2);
}

/////////////////////////////////////////////////////////////////////////////////

//named call

function myAddFunction(uint a, uint b) returns (uint result) {
 return a+b;
}

function fourPlusTwo() returns (uint result) {
 return myAddFunction({b: 2, a:4});
}

기존 함수에서는 함수의 서명을 통해 4,2 순서로 매개변수가 정의된다.

fourPlusTwo함수는 dictionary를 사용해서 myAddFuncrion 서명과 매치시킨다. Dictionary에 따라 순서는 상관이 없어진다.


Inheritance(상속)

 

Solidity는 컨트랙트 간 상속을 지원한다. 이는 부모 컨트랙트의 코드를 자식 컨트랙트(Subcontract)로 복사하여, 단일 바이트코드로 컴파일된 후 블록체인에 배포되는 방식으로 동작한다.
즉, 부모 컨트랙트의 모든 함수와 변수를 자식 컨트랙트에서 사용할 수 있다.

Solidity는 다중 상속도 지원한다.
이 경우 컴파일러는 모든 부모 컨트랙트를 하나로 합쳐 바이트코드를 생성하고 이를 블록체인에 배포한다.
다중 상속 시 동일한 함수가 여러 부모 컨트랙트에 존재할 경우 충돌이 발생할 수 있다. 따라서 Solidity는 C3 선형화(C3 Linearization)를 사용해 상속 순서를 정한다.

 

만약 부모 컨트랙트에 있는 함수가 자식 컨트랙트에도 동일하게 존재한다면, 이 함수는 오버로딩(Overloading) 된다.
두 함수가 동일한 서명을 가지는 경우, 자식 컨트랙트의 함수가 부모 컨트랙트의 함수를 덮어쓴다(Override).
그럼에도 불구하고 부모 함수는 super 키워드를 사용해 명시적으로 호출할 수 있습니다.

 

상속은 블록체인에 배포된 이후, 바이트코드에서 이를 확인할 수 없다.
상속의 사용 여부는 배포된 후의 바이트코드로는 확인할 수 없다.

 

다중 상속

 

C3 선형화는 다음 규칙에 따라 작동한다:

  1. 시작 클래스를 선형화하는 작업을 시작.
  2. 부모 클래스의 리스트를 왼쪽부터 읽기.
  3. 각 부모 클래스가 아직 정렬되지 않았을 경우, 선형화를 반복.
  4. 충돌이 발생하면 가장 왼쪽의 가능한 부모를 선택.

다중상속

 

 

Solidity에서는 is 키워드를 사용해 한 컨트랙트가 다른 컨트랙트를 상속받을 수 있다.
여러 부모 컨트랙트를 상속받을 경우, 컴파일러는 C3 선형화를 사용해 순서를 계산한다.

contract A {}
contract B {}
contract C {}
contract D is A, B {}
contract E is B, C {}
contract F is D, E {}

이 예시에서 F는 D와 E를 상속받고, D와 E는 각각 다른 컨트랙트를 상속받는다.

 

 

FRO(Function Resolution Order)는 함수 호출 시, 어떤 부모 컨트랙트의 함수가 우선 호출될지를 정의한다.

위의 구조에서 FRO 순서는 다음과 같다: FDEABC

 

 

super 키워드는 FRO에 따라 바로 다음 상위 컨트랙트의 함수를 참조한다.
예를 들어:
F에서 super를 호출하면 D를 참조한다.
D에서 super를 호출하면 E를 참조한다.

더보기

예시)  Final.getNumber()이 호출되면 발생하는 일 >>> 함수 순서: Final > C > B > A

super에 의해 Final이 C를 참조, C는 B를 참조, B는 A를 참조한다.

A.getNumber() → 1337 반환.
B.getNumber() → super.getNumber() + 1 → 1337 + 1 = 1338.
C.getNumber() → super.getNumber() + 2 → 1338 + 2 = 1340.

Final에서 return super.getNumber() = C.getNumbeer = 1340

contract A {
 function getNumber() returns (uint a) { 
 return 1337;
 }
}
 
contract B is A {
  function getNumber() returns (uint a) { 
  return super.getNumber() + 1;
 }
}

contract C is A {
 function getNumber() returns (uint a) { 
return super.getNumber() + 2;
 }
}

contract Final is C, B {
 function getNumber() returns (uint a) { 
 return super.getNumber();
 }
}

추상화 컨트랙트 

컨트랙트 내에 함수 본문이 없는 함수(abstract function)가 하나 이상 있을 경우 암시적으로 추상 컨트랙트로 선언된다.

contract CarInsurance {
 function payMonthlyFee() returns (boolean result);
 }

위 예에서 CarInsurance 컨트랙트의 payMonthlyFee() 함수는 본문이 없기 때문에 추상적이다.

 

추상 컨트랙트는 직접 컴파일하여 배포할 수 없다.
따라서, 이를 상속받은 컨트랙트에서 모든 추상 함수를 구현해야만 컴파일 가능하다.
추상 컨트랙트를 상속받은 컨트랙트는 모든 추상 함수를 구현하거나, 자신도 추상 컨트랙트로 남아야 한다.
구현하지 않으면 컴파일러 오류가 발생한다.

 

추상 컨트랙트는 구현과 정의를 분리하여 더 나은 확장성과 유지보수를 제공한다.
특히, 큰 규모의 프로젝트에서 코드 재사용성을 높이고 시스템 설계의 유연성을 확보할 수 있다.

인터페이스

 

인터페이스는 추상 컨트랙트와 비슷하지만, 더 엄격한 제한을 가진다.
모든 함수는 본문이 없는 상태로 선언되며, 해당 함수들은 외부적으로 호출될 수 있다.

생성자(constructor): 인터페이스 안에 정의할 수 없다.(즉, 상태 초기화와 같은 작업은 불가능)
변수(variable): 상태 변수는 선언할 수 없다.
구조체(struct) 및 열거형(enum): 인터페이스 안에 포함될 수 없다.
인터페이스는 컨트랙트로부터 상속받을 수 없고, 다른 인터페이스를 구현할 수도 없습다.

interface CarInsurance {
 func%on payMonthlyFee() returns (boolean result);
}


하나의 컨트랙트는 여러 인터페이스를 동시에 구현할 수 있다.
이는 다양한 규약을 준수하는 컨트랙트를 작성할 때 유용하다.

 

 

 

 

 

 

반응형

'암호 > 블록체인' 카테고리의 다른 글

블록체인 7-3  (2) 2024.12.11
블록체인 7-2  (2) 2024.12.06
블록체인 6-3  (1) 2024.12.05
블록체인 6-2  (1) 2024.12.05
블록체인 6-1  (0) 2024.12.04