[Solidity] Accessing Private Data

임형석·2023년 10월 9일
0

Solidity


Slot

배포한 컨트랙트의 내부에는 코드가 들어있다.

그리고 컨트랙트에 저장하는 상태변수는 각 Slot 에 저장된다.

총 슬롯의 갯수는 2^256 개이다. 따라서 솔리디티는 0 ~ 2**256 -1 의 숫자만을 표현하고 인식할 수 있다.

각 슬롯에는 32bytes 크기의 상태변수를 저장할 수 있다.


Slot 개념

예를 들어, 다음과 같이 4개의 상태변수를 가지고 있는 slot 이라는 컨트랙트가 있다면..

contract slot {
    bytes32 a;
    uint b;
    address c;
    bool d;
}

아래의 사진처럼 상태변수와 슬롯을 정리할 수 있을 것이다.

각 슬롯은 32bytes 의 크기를 가지기에, 0번째 슬롯에는 bytes32 a; 상태변수가 저장된다.

다음 1번째 슬롯에는 uint b 상태변수 14 가 저장된다.

다음 2번째 슬롯에는 address c 상태변수가 저장된다.

여기서, 2번째 슬롯에는 bytes20 크기의 address 형태가 저장되어있다.

그렇다면 12bytes 만큼의 크기가 남게되는데, 이 경우 이웃에 있는 알맞은 크기의 상태변수는 슬롯2 에 저장하게 된다.

다음으로 boolean 타입 상태변수 d 는 오직 1byte 크기이므로, 슬롯2에 저장할 수 있다. 아래의 그림과 같다.


그렇다면 uint 타입의 상태변수를 저장한 슬롯1 의 남는자리는 왜 사용할 수 없을까?

그 이유는, uint 타입의 경우 16진수로 32bytes 모두를 14 라는 수를 인식하기 위해 사용되기 때문에 다른 변수를 슬롯에 할당할 수 없는 것이다.


Private 데이터에 접근하기

상태변수의 저장과 슬롯에 대한 개념을 이해했으니, 아래의 코드를 사용하여 테스트 넷에 배포.

배포 후, truffle console 을 이용하여 슬롯에 직접적으로 접근하여 private 데이터를 읽어온다.

contract accountDB {
    // slot 0
    uint public count = 123; // 32 bytes (2**8) * 32

    // slot 1
    address public owner = msg.sender; // 20 bytes (2**8) * 20
    bool public isTrue = true; // 1 byte
    uint16 public u16 = 30; // 2 bytes (2**8) * 2

    // slot 2
    bytes32 private password;

    // constant 는 slot 에 저장되지 않음.
    uint public constant someConst = 123;

    // slot 3, 4, 5 (하나의 슬롯당 하나의 배열 자리를 차지함)
    bytes32[3] public data;

    /* 
    slot 6 - 배열의 길이
    슬롯에 배열의 요소가 저장됨 어디에? => keccak256(slot 넘버)
    */
    struct User {
        uint id;
        bytes32 password;
    }
    User[] private users;

    /* 
    slot 7
    슬롯 7 맵핑의 슬롯 => keccak256(mapping key, slot 넘버)
    */
    mapping(uint => User) private idToUser;

    constructor(bytes32 _password) {
        password = _password;
    }

    function addUser(bytes32 _password) public {
        User memory user = User({
            id: users.length,
            password: _password
        });
        users.push(user);
        idToUser[user.id] = user;
    }

먼저, truffle 을 이용하여 컨트랙트를 배포해야한다.

배포전, 1_A.js 파일을 migrations 폴더 내에 생성하고, 아래와 같이 코드를 작성한다. private 패스워드는 "0xabc123" 이다.

const accountDB = artifacts.require("accountDB");
module.exports = function (deployer) {
  deployer.deploy(accountDB, "0xabc123");
};

아래의 명령어를 입력하여 고엘리 테스트 네트워크에 배포한다.
truffle migrate --network goerli

배포가 되었다면 콘솔창이 아래와 같은 정보를 표시한다.

다음 단계로, truffle console --network goerli 를 입력.

배포한 컨트랙트를 지정한다. const A = await accountDB.deployed()

addUser 함수를 이용해 첫번째 유저를 생성한다. A.addUser("0x111aaabbb")

여기까지 완료되면 기본 세팅은 끝났다.


슬롯 0.

uint public count = 123;

getStorageAt 을 사용하여 0번째 슬롯의 데이터를 불러온다.

web3.eth.getStorageAt(addr, 0, console.log)

7b 가 나온다.

parseInt 로 파싱하면 슬롯 0에 저장된 것은 uint count 상태변수는 123 임을 알 수 있다.


슬롯 1.

address public owner = msg.sender;
bool public isTrue = true;
uint16 public u16 = 30;

1e, 01, 0x9d8E21A936D09Ffdd2963B0795Af581849D849Ab

오른쪽에서부터 owner, isTrue, u16 이다.


슬롯 2.

배포할 때 사용한 "abc123" password 가 private 값이지만 그대로 출력된다.


슬롯 6.

3,4,5 는 없으므로 슬롯 6을 확인. 슬롯 6은 private. User 구조체이다.

    struct User {
        uint id;
        bytes32 password;
    }
    User[] private users;

슬롯 6에 id 값 1을 확인.

슬롯 6은 struct 구조의 변수를 저장하기에 soliditySha3 를 이용하여 hash 값을 찾아내고, hash 값을 이용하여 슬롯을 확인한다.

슬롯에 할당된 값이 없는 것 처럼 보인다. 하지만 이건 id 값이 0 이기에 그렇다.

hash 값에서 1을 더해준다면 처음에 addUser 함수로 생성한 private 인 id, password 값을 불러낼 수 있다.

이 hash 값에서 1을 더해준다면, 2번째 유저의 아이디를 확인할 수 있고, 그 값에서 또 1을 더해준다면 2번째 유저의 password 를 확인할 수 있다.

hash 뒷자리 [3f, 40] => 1번째 유저 id, password hash
hash 뒷자리 [41, 42] => 2번째 유저 id, password hash...

라고 생각하면 되겠다.


슬롯 7.

슬롯 7은 mapping(uint => User) private idToUser; private mapping 이다.

mapping 의 슬롯의 계산 => keccak256(mapping key, 슬롯 넘버)

따라서 hash 값은 web3.utils.soliditySha3({ type: "uint", value: 1},{ type: "uint", value: 7})

hash 값으로 7번 슬롯 mapping key 1 의 id 값을 확인.

hash 값 + 1 하여 7번 슬롯 mapping key 1 의 password 값 확인.


정리

이전에는 컨트랙트를 상속하거나 인스턴스화 하여서 private 변수에 접근했지만 결국엔 접근하지 못했었다.

하지만 이렇게 간단하게 접근할 수 있을거라곤 생각도 못했다.

중요한 것은, 민감한 데이터는 반드시 블록체인이 아닌 암호화된 db 에 저장해야 한다는 것.

이번에 공부했던 내용 중에서 mapping, struct 의 private 값에 접근하는 방법이 조금 까다로웠다. 위의 슬롯 6과 7을 다시 읽어보니 두서가 없는 것 같아 요약 정리해보았다.

  • Struct

hash => web3.utils.soliditySha3({type: "uint", value : slot넘버})

Struct 의 첫 번째 변수 => web3.eth.getStorageAt(addr, hash, console.log)

Struct 의 두 번째 변수 => web3.eth.getStorageAt(addr, hash+1, console.log)

Struct 의 세 번째 변수 => web3.eth.getStorageAt(addr, hash+2, console.log) ...

  • mapping

hash => web3.utils.soliditySha3({type: "uint", value : mapping key}, {type: "uint", value : slot넘버})

mapping struct 의 첫 번째 변수 => web3.eth.getStorageAt(addr, hash, console.log)

mapping struct 의 두 번째 변수 => web3.eth.getStorageAt(addr, hash +1, console.log)

mapping struct 의 세 번째 변수 => web3.eth.getStorageAt(addr, hash +2, console.log)


0개의 댓글