이더리움에서는 디지털 서명을 세 가지 용도로 사용된다.
트랜잭션 생성 시 ECDSA 를 통해 메시지를 디지털 서명을 할 수 있으며, 서명 된 트랜잭션을 전송한다.(ECDSA에 대한 설명은 생략)
트랜잭션 발생을 위한 7가지 과정
※ EIP-155
'단순 재생 공격 방지(Simple Replay Attack Protection)' 표준은 서명 하기 전에 트랜잭션 데이터 내부에 체인 식별자(chain identifier)를 포함하여 인코딩한다.
서명에 체인 식별자가 포함됨으로써 다른 체인에서 트랜잭션을 수행할 수 없다.
EIP-155 구조는 주요 6개 필드에 chainID, 0, 0 3개 필드를 추가한다.
※ v 변수
ECDSArecover 함수가 서명을 확인하는데 도움이 되는 복구 식별자와 체인ID이다.
v는 27 또는 28 중 하나로 계산되거나, 체인ID * 2 + 35 또는 36(signature->recoveryParam)으로 계산된다.
서명확인은 서명(r, s)과 시리얼라이즈된 트랜잭션, 서명을 만드는데 사용된 개인키에 상응하는 공개키가 있으면 된다.
// 1. [nonce, gasPrice, gasLimit, to, value, data]의 6개 필드를 포함하는 트랜잭션 데이터 구조 생성한다
const formattedNonce = nonce === 0 ? "0x" : ethers.utils.hexlify(nonce)
const transactionData = {
nonce: formattedNonce,
gasPrice: 875000000,
gasLimit: 21000,
to: toAccount,
value: 10,
data: "0x"
};
// 2. RLP로 인코딩된 트랜잭션 데이터 구조의 시리얼라이즈된 메시지를 생성한다
const rlpData = [
transactionData.nonce,
ethers.utils.hexlify(transactionData.gasPrice),
ethers.utils.hexlify(transactionData.gasLimit),
ethers.utils.hexlify(transactionData.to),
ethers.utils.hexlify(transactionData.value),
transactionData.data
];
const serializeData = ethers.utils.RLP.encode(rlpData);
// serializeTransaction 함수를 통해서도 가능
const serializeData2 = ethers.utils.serializeTransaction(transactionData);
assert.equal(serializeData, serializeData2)
// 3. 시리얼라이즈된 메시지의 Keccak-256 해시를 계산한다
const digest = ethers.utils.keccak256(serializeData);
// 4. EOA의 개인키로 해시에 서명하여 ECDSA 서명을 계산
const signingKey = new ethers.utils.SigningKey(privateKey);
const signature = signingKey.signDigest(digest);
// 5. ECDSA 서명의 계산된 v, r, s 값을 트랜잭션에 추가
const { v, r, s } = ethers.utils.splitSignature(signature);
const signRlpData = [
ethers.utils.hexlify(transactionData.nonce),
ethers.utils.hexlify(transactionData.gasPrice),
ethers.utils.hexlify(transactionData.gasLimit),
transactionData.to,
ethers.utils.hexlify(transactionData.value),
transactionData.data,
ethers.utils.hexlify(v),
r,
s
];
// 6. 서명이 추가된 트랜잭션 데이터를 RLP 인코딩해 시리얼라이즈하여 전송한다.
const signData= ethers.utils.RLP.encode(signRlpData);
// 7. 트랜잭션 전송
const sentTransaction = await ethers.provider.sendTransaction(signData);
// 1. [nonce, gasPrice, gasLimit, to, value, data]의 6개 필드를 포함하는 트랜잭션 데이터 구조 생성한다
const formattedNonce = nonce === 0 ? "0x" : ethers.utils.hexlify(nonce)
const transactionData = {
nonce: formattedNonce,
gasPrice: 875000000,
gasLimit: 21000,
to: toAccount,
value: 10,
data: "0x"
};
// 2, 3, 4, 5, 6 = > 트랜잭션 서명
const signedTransaction = await wallet.signTransaction(transactionData);
// 7. 트랜잭션 전송
const sentTransaction = await ethers.provider.sendTransaction(signedTransaction);
// 1. 트랜잭션 데이터를 갖고온다.
const transaction = await ethers.provider.getTransaction(transactionHash);
// 2. 트랜잭션내에 서명값을 추출한다.(r, s) (v도 추출)
const v = ethers.utils.hexStripZeros(transaction.v);
const r = ethers.utils.hexZeroPad(transaction.r, 32);
const s = ethers.utils.hexZeroPad(transaction.s, 32);
// 3. RLP로 인코딩된 트랜잭션 데이터 구조의 시리얼라이즈된 메시지를 생성한다.
const nonce = transaction.nonce === 0 ? "0x" : ethers.utils.hexlify(transaction.nonce)
const rlpData = [
nonce,
ethers.utils.hexlify(transaction.gasPrice),
ethers.utils.hexlify(transaction.gasLimit),
ethers.utils.hexlify(transaction.to),
ethers.utils.hexlify(transaction.value),
transaction.data
];
const signData = ethers.utils.RLP.encode(rlpData);
// 4. 시리얼라이즈된 메시지의 Keccak-256 해시를 계산한다.
const messageHash = ethers.utils.keccak256(signData);
// 5. ECDSA 서명의 계산된 v, r, s 을 통해 서명 공개키를 복구한다.
const pubKey = ethers.utils.recoverPublicKey(messageHash, { v, r, s });
// 6. 서명 공개키의 이더리움 주소를 얻은 후 개인키에 상응하는 공개키랑 비교한다.
const signerAddress = ethers.utils.computeAddress(pubKey);
if( transaction.from == signerAddress )
return true
// 1. [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]의 9개 필드를 포함하는 트랜잭션 데이터 구조 생성한다
const formattedNonce = nonce === 0 ? "0x" : ethers.utils.hexlify(nonce)
const transactionData = {
nonce: formattedNonce,
gasPrice: gasPrise,
gasLimit: 30000000,
to: toAccount,
value: 10,
data: "0x",
chainId: chainId
};
// 2. RLP로 인코딩된 트랜잭션 데이터 구조의 시리얼라이즈된 메시지를 생성한다
const rlpData = [
transactionData.nonce,
ethers.utils.hexlify(transactionData.gasPrice),
ethers.utils.hexlify(transactionData.gasLimit),
ethers.utils.hexlify(transactionData.to),
ethers.utils.hexlify(transactionData.value),
transactionData.data,
ethers.utils.hexlify(transactionData.chainId),
"0x",
"0x"
];
const serializeData = ethers.utils.RLP.encode(rlpData);
// serializeTransaction 함수를 통해서도 가능
const serializeData2 = ethers.utils.serializeTransaction(transactionData);
assert.equal(serializeData, serializeData2)
// 3. 시리얼라이즈된 메시지의 Keccak-256 해시를 계산한다
const digest = ethers.utils.keccak256(serializeData);
// 4. EOA의 개인키로 해시에 서명하여 ECDSA 서명을 계산
const signingKey = new ethers.utils.SigningKey(privateKey);
const signature = signingKey.signDigest(digest);
// 5. ECDSA 서명의 계산된 v, r, s 값을 트랜잭션에 추가
const { r, s } = ethers.utils.splitSignature(signature);
const signRlpData = [
ethers.utils.hexlify(transactionData.nonce),
ethers.utils.hexlify(transactionData.gasPrice),
ethers.utils.hexlify(transactionData.gasLimit),
transactionData.to,
ethers.utils.hexlify(transactionData.value),
transactionData.data,
ethers.utils.hexlify(transactionData.chainId*2+(35+signature.recoveryParam)),
r,
s
];
// 6. 서명이 추가된 트랜잭션 데이터를 RLP 인코딩해 시리얼라이즈하여 전송한다.
const signData= ethers.utils.RLP.encode(signRlpData);
// 7. 트랜잭션 전송
const sentTransaction = await ethers.provider.sendTransaction(signData);
// 1. [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]의 9개 필드를 포함하는 트랜잭션 데이터 구조 생성한다
const formattedNonce = nonce === 0 ? "0x" : ethers.utils.hexlify(nonce)
const transactionData = {
nonce: formattedNonce,
gasPrice: gasPrise,
gasLimit: 30000000,
to: toAccount,
value: 10,
data: "0x",
chainId: chainId
};
// 2, 3, 4, 5, 6 = > 트랜잭션 서명
const signedTransaction = await wallet.signTransaction(transactionData);
// 7. 트랜잭션 전송
const sentTransaction = await ethers.provider.sendTransaction(signedTransaction);
// 1. 트랜잭션 데이터를 갖고온다.
const transaction = await ethers.provider.getTransaction(transactionHash);
// 2. 트랜잭션내에 서명값을 추출한다.(r, s) (v도 추출)
const v = ethers.utils.hexStripZeros(transaction.v);
const r = ethers.utils.hexZeroPad(transaction.r, 32);
const s = ethers.utils.hexZeroPad(transaction.s, 32);
// joinSignature 함수를 통해서도 가능
const expandedSig = {
r: transaction.r,
s: transaction.s,
v: transaction.v
};
// recoverPublicKey(messageHash, signature)
const signature = ethers.utils.joinSignature(expandedSig);
// 3. RLP로 인코딩된 트랜잭션 데이터 구조의 시리얼라이즈된 메시지를 생성한다.
const nonce = transaction.nonce === 0 ? "0x" : ethers.utils.hexlify(transaction.nonce)
const rlpData = [
nonce,
ethers.utils.hexlify(transaction.gasPrice),
ethers.utils.hexlify(transaction.gasLimit),
ethers.utils.hexlify(transaction.to),
ethers.utils.hexlify(transaction.value),
transaction.data,
ethers.utils.hexlify(transaction.chainId),
"0x",
"0x"
];
const signData = ethers.utils.RLP.encode(rlpData);
// 4. 시리얼라이즈된 메시지의 Keccak-256 해시를 계산한다.
const messageHash = ethers.utils.keccak256(signData);
// 5. ECDSA 서명의 계산된 v, r, s 을 통해 서명 공개키를 복구한다.
const pubKey = ethers.utils.recoverPublicKey(messageHash, { v, r, s });
// 6. 서명 공개키의 이더리움 주소를 얻은 후 개인키에 상응하는 공개키랑 비교한다.
const signerAddress = ethers.utils.computeAddress(pubKey);
if( transaction.from == signerAddress )
return true
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyContract {
mapping(uint256 => bool) usedNonces;
uint256 private chainId;
address private signVerifier;
uint256 public values;
constructor(address _signVerifier, uint256 _chainId){
chainId = _chainId;
signVerifier = _signVerifier;
}
function setValues(uint256 _values, uint256 _nonce, bytes memory _sig) external {
require(!usedNonces[_nonce], "Signature Has Already Been Used");
usedNonces[_nonce] = true;
bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, _values, _nonce, chainId, this)));
require(recoverSigner(message, _sig) == signVerifier, "Invalid Signature");
values = _values;
}
function recoverSigner(bytes32 message, bytes memory sig)
public
pure
returns (address)
{
uint8 v;
bytes32 r;
bytes32 s;
(v, r, s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
function splitSignature(bytes memory sig)
public
pure
returns (uint8, bytes32, bytes32)
{
require(sig.length == 65);
bytes32 r;
bytes32 s;
uint8 v;
assembly {
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
const MessageData = {
account : accounts[0].address,
data : 10,
nonce : await accounts[0].getTransactionCount(),
chainId : chainId,
contract : myContract.address
}
const encodedData = ethers.utils.keccak256(ethers.utils.solidityPack(
["address", "uint256", "uint256", "uint256", "address"],
[MessageData.account, MessageData.data, MessageData.nonce, MessageData.chainId, MessageData.contract]
));
const wallet = new ethers.Wallet(privateKey);
const signature = await wallet.signMessage(ethers.utils.arrayify(encodedData));
await myContract.setValues(
MessageData.data,
MessageData.nonce,
signature
)
https://devkly.com/blockchain/raw-transaction/
https://programtheblockchain.com/posts/2018/02/17/signing-and-verifying-messages-in-ethereum/
<마스터링 이더리움>
https://github.com/4e5ung/solidity-study/tree/main/ecdsa_transaction