‘이더리움 댑 개발’ 세미나 13. 솔리디티 공식 문서(버전 0.8.x) – Internals

이번 세미나에서는 Internals 부분을 다룹니다.

Internals에서 Source mappings, The Optimizer, Contract Metadata는 생략했고, Contract ABI Specification은 일부 내용만 정리했습니다.

 

Layout of State Variables in Storage

  • 컨트랙트의 상태 변수들은 첫 번째 상태변수부터 순차적으로 스토리지에 저장됩니다.
  • 스토리지(storage)는 슬롯(slot)들로 구성됩니다.
  • 하나의 슬롯은 32바이트 크기를 갖습니다. 첫 번째 슬롯은 0에서 시작합니다.
  • 32바이트보다 작은 값들은 가능한 경우 하나의 슬롯에 다수의 값이 저장될 수 있습니다.
  • 변수 값의 크기는 변수 타입에 의해 결정됩니다.
    • 값 타입은 저장에 필요한 만큼의 바이트만 사용합니다.
    • 값 타입이 스토리지 슬롯의 남은 공간 보다 클 경우 다음 스토리지 슬롯에 저장됩니다.
    • 구조체와 배열 데이터는 항상 새로운 슬롯에서 시작합니다. 구조체와 배열 요소들은 위에서 언급한 것과 같은 규칙에 따라 저장됩니다.
      • The elements of structs and arrays are stored after each other, just as if they were given as individual values.
  • 컨트랙트는 상속을 지원하는데 상태 변수의 순서는 최상위 컨트랙트부터 C3-linearized 순서를 따릅니다.
  • 32바이트보다 작은 크기의 상태 변수 값이 사용한다고 항상 잇점이 있는 것은 아닙니다. 
    • EVM은 한 번에 32바이트 단위로 연산을 실행합니다. 32바이트 보다 작은 값인 경우 EVM은 크기를 줄이기 위해 더 많은 연산을 해야 합니다.
    • 하나의 슬롯에 다수의 값을 저장한 경우 한 슬롯에 저장된 값을 동시에 쓰고 읽어야 한다면 이익이 되지만, 그렇지 않다면 반대 효과를 가져올 수 있습니다.
    • 함수 인자나 메모리 값을 다룰 때는 컴파일러가 이들 값을 pack하지 않습니다. 따라서 32바이트보다 작은 크기의 타입을 사용한다고 해도 pack함으로 얻을 수 있는 잇점은 없습니다.
    • 32바이트 단위로 pack되도록 상태 변수들의 크기를 고려해 선언 순서를 정해주어야 합니다.
  • 구조체는 하나의 단위로 다뤄진다고 볼 수 있기 때문에 하나의 슬롯에 다수의 값을 저장하는 것이 유리합니다.
    • 속성은 선언 순서대로 저장되기 때문에 속성 선언 순서에 주의해야 합니다.
    • uint128, uint128, uint256로 선언하면 2개의 슬롯을 사용하지만 uint128, uint256, uint128로 선언하면 세 개의 슬롯을 사용하게 됩니다.
  • The layout of state variables in storage is considered to be part of the external interface of Solidity due to the fact that storage pointers can be passed to libraries. This means that any change to the rules outlined in this section is considered a breaking change of the language and due to its critical nature should be considered very carefully before being executed.
  • 매핑과 동적 배열은 크기를 예측할 수 없기 때문에 다른 상태 변수들 사이에 저장될 수 없습니다.
  • 매핑과 동적 배열은 Keccak-256 해시 값만 위에서 언급한 레이아웃 규칙에 따라 저장됩니다. 이 해시 값은 매핑이나 배열 요소가 저장되는 위치입니다.
    • 매핑과 동적 배열은 크기를 예측할 수 없기 때문에 배열 데이터가 저장되는 슬롯 위치만 저장합니다.
      • 매핑이나 배열의 저장소 위치는 스토리지 레이아웃 규칙이 적용된 후  slot p가 마지막 슬롯이라고 가정합니다.
        • 동적 배열에서 p는 배열 요소의 갯수를 나타냅니다.
          • 동적 배열에서 p는 배열 요소의 갯수에 해당합니다.
            • 바이트 배열과 문자열은 예외
            • 매핑의 경우 슬롯은 빈 상태가 됩니다.
              • It is still needed to ensure that even if there are two mappings next to each other, their content ends up at different storage locations.
  • 배열 데이터는 keccak256(p)에서 시작해서 정적 크기 배열 데이터에서와 같은 방법으로 레이아웃 됩니다.
    • One element after the other, potentially sharing storage slots if the elements are not longer than 16 bytes.
  • 동적 배열의 동적 배열은 배열 데이터의 레이아웃 규칙을 재귀적으로 적용합니다.
    • x의 타입이 uint24[][]일 때 x[i][j]의 위치는 다음과 같이 계산된다. x의 위치를 slot p라고 가정할 때
      • 슬롯은 keccak256(keccak256(p) + i) + floor(j / floor(256 / 24))이다.
      • 배열 요소는 슬롯 데이터 v로 부터 얻어진다.
        • v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max.
  • 매핑 키 k의 값은 keccak256(h(k) . p)에 위치합니다.
    • .는 결합 연산자이고
    • h는 타입에 따라 키에 적용되는 함수입니다.
      • 값 타입의 경우 메모리에 값을 저장할 때와 마찬가지 방법으로 32바이트로 값을 pad합니다.
      • 문자열이나 바이트배열의 경우 unpadded 데이터의 keccak256 해시 입니다.
      • 값 타입이 아닌 경우, 계산된 슬롯은 데이터의 시작을 표시합니다. 예를 들어 구조체인 경우 구조체 멤버에 해당하는 offset을 추가해서 멤버에 도착합니다.
    • 예) data[4][9].c의 저장소 위치 구하기
        • 매핑 자체의 위치는 1입니다.
        • data[4]의 위치는 keccak256(uint256(4) . uint256(1))입니다.
        • data[4][9]의 위치는 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1)))
        • data[4][9].c의 위치는 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1
  • bytes와 string은 같은 방법으로 인코딩됩니다.
    • byte1[]과 유사합니다
      • 배열 자체에 대한 하나의 슬롯이 있고, 그 슬롯의 위치에 대한 keccak256 해시를 사용해서 데이터 영역을 계산합니다.
      • 그러나 짧은 값 (32 바이트 미만)의 경우 배열 요소는 길이와 함께 동일한 슬롯에 저장됩니다.
      • 31바이트 이하이면 상위 비트 부터 요소들을 저장하고 최하위 바이트에 length * 2를 저장합니다. 32바이트 이상이면 length * 2 + 1 값을 저장합니다.
        • 최하위 비트가 설정되었는지에 따라 32바이트 보다 작은 지 큰지를 구분합니다.
  • Handling invalidly encoded slots is currently not supported but may be added in the future. If you are compiling via the experimental IR-based compiler pipeline, reading an invalidly encoded slot results in a Panic(0x22) error.
  • 컨트랙트 스토리지 레이아웃은 표준 JSON 인터페이스로 요청될 수 있습니다.
    •  최상위 객체 이름은 “storageLayout” 입니다.
      • storage와 types라는 두 개의 키를 갖습니다.
        • storage 객체는 각각의 요소가 다음과 같은 형식을 갖는 배열입니다.
          • astId
            • is the id of the AST node of the state variable’s declaration
          • contract
            • is the name of the contract including its path as prefix
          • label
            • is the name of the state variable
          • offset
            • is the offset in bytes within the storage slot according to the encoding
          • slot
            • is the storage slot where the state variable resides or starts. This number may be very large and therefore its JSON value is represented as a string.
          • type
            • is an identifier used as key to the variable’s type information (described in the following)
        • The given type, in this case t_uint256 represents an element in types, which has the form:
          • encoding
            • how the data is encoded in storage, where the possible values are:
              • inplace
                • data is laid out contiguously in storage.
              • mapping
                • Keccak-256 hash-based method.
              • dynamic_array
                • Keccak-256 hash-based method.
              • bytes
                • single slot or Keccak-256 hash-based depending on the data size.
          • label
            • is the canonical type name.
          • numberOfBytes
            • is the number of used bytes (as a decimal string). Note that if numberOfBytes > 32 this means that more than one slot is used.
        • Some types have extra information besides the four above. Mappings contain its key and value types (again referencing an entry in this mapping of types), arrays have its base type, and structs list their members in the same format as the top-level storage.
    • The JSON output format of a contract’s storage layout is still considered experimental and is subject to change in non-breaking releases of Solidity.

Layout in Memory

  • 특별한 용도로 예약된 세 영역이 있습니다. 이 영역(영역의 끝을 포함하는)에 데이터 값을 쓰지 않도록 주의 해야 합니다.
    • 0x00 – 0x3f (64 bytes): scratch space for hashing methods
      • Scratch space can be used between statements (i.e. within inline assembly).
    • 0x40 – 0x5f (32 bytes): currently allocated memory size (aka. free memory pointer)
    • 0x60 – 0x7f (32 bytes): zero slot
      • The zero slot is used as initial value for dynamic memory arrays and should never be written to (the free memory pointer points to 0x80 initially).
  • 데이터 값은 0x80에서 부터 쓸 수 있기 때문에 free memory pointer는 초기에 0x80을 가르킵니다.
    • 새로운 객체는 free memory pointer에 위치합니다.
  • 메모리는 결코 freed되지 않습니다(this might change in the future).
  • 배열 요소들은 스토리지와는 달리 타입에 상관 없이 32바이트 크기를 차지합니다. 구조체 속성들과 byte[]에 대해서도 마찬가지 입니다. string과 bytes는 제외합니다.
  • Multi-dimensional memory arrays are pointers to memory arrays. The length of a dynamic array is stored at the first slot of the array and followed by the array elements.
  • There are some operations in Solidity that need a temporary memory area larger than 64 bytes and therefore will not fit into the scratch space. They will be placed where the free memory points to, but given their short lifetime, the pointer is not updated. The memory may or may not be zeroed out. Because of this, one should not expect the free memory to point to zeroed out memory. While it may seem like a good idea to use msize to arrive at a definitely zeroed out memory area, using such a pointer non-temporarily without updating the free memory pointer can have unexpected results.

Layout of Call Data

  • 외부 함수 호출에서 입력 데이터는 ABI 명세의 의해 정의된 형식으로 되어 있을 것으로 가정됩니다. ABI 명세는 인자들이 다수의 32바이트로 padded되어질 것을 요구합니다.
  • 생성자 인자는 컨트랙트의 코드 끝에 직접적으로 추가됩니다.
    • The constructor will access them through a hard-coded offset, and not by using the codesize opcode, since this of course changes when appending data to the code.

Cleaning Up Variables

  • 값이 256비트 보다 짧다면 어떤 경우에는 나머지 비트는 cleaned되어야 합니다. 솔리디티 컴파일러는 그러한 나머지 비트를 연산 전에 clean하도록 설계되었습니다.
    • 바로 다음 연산이 영향을 받지 않으면 비트를 clean하지 않습니다.
      • 예를 들어, 0이 아닌 값이 JUMPI instruction에 의해 true로 간주된다면 clean되지 않습니다.
  • 스택에 입력 데이터를 로드할 때 솔리디티 컴파일러는 clean합니다.

 

Different types have different rules for cleaning up invalid values:

Type Valid Values Invalid Values Mean
enum of n members 0 until n – 1 exception
bool 0 or 1 1
signed integers sign-extended word currently silently wraps; in the future exceptions will be thrown
unsigned integers higher bits zeroed currently silently wraps; in the future exceptions will be thrown

Contract ABI Specification

  • ABI는 이더리움 생태계에서 블록체인 외부에서와 컨트랙트 대 컨트랙트의 상호작용에서 컨트랙트와 상호작용하는 표준적인 방법입니다.데이터는 타입에 따라 명세에 기술된 대로 인코딩됩니다. 인코딩은 self describing이 아니라 디코드하기 위한 스키마가 요구됩니다.
  • 컨트랙트의 인터페이스 함수가 strongly typed되었고 컴파일타임 시에 알려져있어야 하고, 정적이라고 가정합니다. 모든 컨트랙트들은 컴파일 타임 시에 호출이 가능한 컨트랙트의 인터페이스 정의를 가지고 있다고 가정합니다.
  • 함수의 호출 데이터의 첫 4바이트는 호출된 함수를 나타냅니다.
    • 함수 시그너처의 Keccak-256 해시의 첫 4바이트
  • 시그니처는 데이터 위치 지정자가 없는 기본 프로토 타입의 표준 표현식, 즉 괄호로 묶인 매개 변수 유형 목록이있는 함수 이름으로 정의됩니다. 매개 변수 유형은 단일 쉼표로 분할되며 공백이 사용되지 않습니다.
    • 다음과 같은 경우를 제외하고 타입은 타입 이름을 그대로 사용합니다.
      • address payable, contract – address
      • enum – uint8
      • struct – tuple
  • 함수의 리턴 타입은 시그너처의 일부가 아닙니다. 그러나 ABI의 JSON 설명에는 입력과 출력이 모두 포함됩니다.
  • 다섯번째 바이트부터 인자들이 인코딩됩니다.

 

About the Author
(주)뉴테크프라임 대표 김현남입니다. 저에 대해 좀 더 알기를 원하시는 분은 아래 링크를 참조하세요. http://www.umlcert.com/kimhn/

Leave a Reply

*