배포한 컨트랙트의 내부에는 코드가 들어있다.
그리고 컨트랙트에 저장하는 상태변수는 각 Slot 에 저장된다.
총 슬롯의 갯수는 2^256 개이다. 따라서 솔리디티는 0 ~ 2**256 -1 의 숫자만을 표현하고 인식할 수 있다.
각 슬롯에는 32bytes 크기의 상태변수를 저장할 수 있다.
예를 들어, 다음과 같이 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 라는 수를 인식하기 위해 사용되기 때문에 다른 변수를 슬롯에 할당할 수 없는 것이다.
상태변수의 저장과 슬롯에 대한 개념을 이해했으니, 아래의 코드를 사용하여 테스트 넷에 배포.
배포 후, 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")
여기까지 완료되면 기본 세팅은 끝났다.
uint public count = 123;
getStorageAt 을 사용하여 0번째 슬롯의 데이터를 불러온다.
web3.eth.getStorageAt(addr, 0, console.log)
7b 가 나온다.
parseInt 로 파싱하면 슬롯 0에 저장된 것은 uint count 상태변수는 123 임을 알 수 있다.
address public owner = msg.sender;
bool public isTrue = true;
uint16 public u16 = 30;
1e, 01, 0x9d8E21A936D09Ffdd2963B0795Af581849D849Ab
오른쪽에서부터 owner, isTrue, u16 이다.
배포할 때 사용한 "abc123" password 가 private 값이지만 그대로 출력된다.
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은 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을 다시 읽어보니 두서가 없는 것 같아 요약 정리해보았다.
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) ...
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)