Ethereum 스테이킹 튜토리얼

DSRV에서 ETH를 스테이킹하세요.

새로운 검증자 키를 생성하고, 해당 검증자에 ETH를 예치하기 위한 스테이킹 트랜잭션을 생성·서명·전파하는 방식으로 진행됩니다.

  1. DSRV API를 통해 검증자 키 생성 및 예치용 unsigned 트랜잭션을 요청
  2. 반환된 트랜잭션을 사용자 지갑에서 직접 서명
  3. 서명된 트랜잭션을 DSRV를 통해 네트워크에 브로드캐스트
  4. 트랜잭션 해시를 기준으로 처리 상태 확인

1. 예치 스테이킹 트랜잭션 요청

검증자 키 생성(할당)과 함께, 예치에 필요한 deposit data 및 unsigned 트랜잭션을 요청합니다. 이 단계에서는 실제 서명이나 전송은 발생하지 않으며, 스테이킹에 필요한 트랜잭션 정보만 생성됩니다.

반환되는 unsignedTransaction은 이후 단계에서 지갑 서명에 사용되므로 반드시 저장해두어야 합니다.

요청 본문 예시:

const response = await fetch(
  `https://api.dsrv.com/api/v1/staking/ethereum/hoodi/request-deposit`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      pubkeyCount: 1,
      ethAmount: 32,
      withdrawAddress: WITHDRAW_ADDRESS,
      requesterAddress: REQUESTER_ADDRESS,
    }),
  },
);

if (!response.ok) {
  const errorText = await response.text();
  throw new Error(`Request Deposit Fail: ${response.status} - ${errorText}`);
}

const responseData = await response.json();

// 다음 단계에서 사용할 unsignedTransaction 저장
const unsignedTransaction = responseData.data.result.unsignedTransaction;

ethAmountpubkeyCount 의미

일반적으로 validator 1개당 32 ETH를 예치합니다.

  • 여러 validator를 한 번에 예치하려면 pubkeyCount를 늘리세요.
  • ethAmount32 ETH로 요청하는 것을 권장합니다.
    • 총 예치금은 보통 32 ETH × pubkeyCount로 구성됩니다.

응답 예시:

{
  "statusCode": 200,
  "data": {
    "result": {
      "requestId": "212d464e-0acd-4e5e-b234-64d12efe9827",
      "assigned": true,
      "assignedCount": 1,
      "message": "Deposit data generated successfully",
      "depositData": [
        {
          "pubkey": "0x81e270729b953b3ab80078d6565bf000bcd2222a50f6d0dcee0b577de73b0e788d2d0ccd4aaa93bff8eb5de243371ad8",
          "withdrawalCredentials": "0x010000000000000000000000B9FF5a65980Ad8D4b6955288cee93184CcCb33f1",
          "amount": "32000000000",
          "signature": "0xabcd1234....",
          "depositMessageRoot": "0x1234abcd....",
          "depositDataRoot": "0xef012345....",
          "forkVersion": "0x10000910",
          "networkName": "hoodi"
        }
      ],
      "unsignedTransaction": {
        "to": "0x.... (네트워크별로 달라질 수 있으므로 응답값 그대로 사용)",
        "data": "0x4....",
        "value": "0x1bc16d674ec800000",
        "network": "hoodi"
      }
    }
  }
}

응답 필드 설명

필드타입설명
requestIdstring요청 추적을 위한 고유 식별자
assignedbooleanpubkey 할당 성공 여부
assignedCountnumber할당된 pubkey 개수
messagestring처리 결과 메시지
depositDataarray예치 데이터 배열
depositData[].pubkeystring밸리데이터 공개 키 (0x 접두사, 48 bytes)
depositData[].withdrawalCredentialsstring출금 자격 증명 (0x01 접두사 + 주소)
depositData[].amountstring예치 금액 (gwei 단위)
depositData[].signaturestringBLS 서명
depositData[].depositMessageRootstring예치 메시지 머클 루트
depositData[].depositDataRootstring예치 데이터 머클 루트
depositData[].forkVersionstring네트워크 포크 버전
depositData[].networkNamestring네트워크 이름
unsignedTransactionobject서명되지 않은 트랜잭션 객체

(중요) 단위/호출 대상 주의

  • depositData[].amount 단위: gwei (32 ETH = 32000000000)
  • unsignedTransaction.value 단위: wei
    • value는 16진수(hex) 문자열(0x...)로 반환됩니다.
    • 예: 32 ETH(wei) = 0x1bc16d674ec800000
  • 호출 대상 컨트랙트 주소(unsignedTransaction.to): 네트워크(및 환경)에 따라 달라질 수 있습니다.
    • 따라서 고객은 반드시 API가 반환한 unsignedTransaction.to 값을 그대로 사용해 트랜잭션을 서명해야 합니다.
    • (Deposit Contract 주소나 예시 주소를 임의로 넣어 서명하면 실패할 수 있습니다.)

2. 예치 스테이킹 트랜잭션 서명

API가 준 unsignedTransaction을 사용자 지갑에서 직접 서명합니다. 개인 키는 DSRV에 전달되지 않습니다.

아래 예시는 Legacy(Type-0) 서명 방식입니다.

import { privateKeyToAccount } from 'viem/accounts';
import { createWalletClient, createPublicClient, http } from 'viem';
import { hoodi } from 'viem/chains';

const RPC_URL = 'https://rpc.hoodi.ethpandaops.io';
const PRIVATE_KEY = '0xYourWalletPrivateKey';

// 1단계에서 받은 unsignedTransaction을 그대로 사용하세요.
const unsignedTransaction = {
  to: '0x... (API 응답값 그대로)',
  data: '0x...',
  value: '0x1bc16d674ec800000', // wei (hex string)
};

const account = privateKeyToAccount(PRIVATE_KEY);

const publicClient = createPublicClient({
  chain: hoodi,
  transport: http(RPC_URL),
});

const walletClient = createWalletClient({
  account,
  chain: hoodi,
  transport: http(RPC_URL),
});

// nonce
const nonce = await publicClient.getTransactionCount({ address: account.address });

// gas estimate
const gas = await publicClient.estimateGas({
  account: account.address,
  to: unsignedTransaction.to,
  data: unsignedTransaction.data,
  value: BigInt(unsignedTransaction.value),
});

// legacy gasPrice
const gasPrice = await publicClient.getGasPrice();

const signedTransaction = await walletClient.signTransaction({
  to: unsignedTransaction.to,
  data: unsignedTransaction.data,
  value: BigInt(unsignedTransaction.value),
  nonce,
  gas,
  gasPrice,
});

// signedTransaction 예: '0x02f9...'(혹은 '0xf8...' 등)
console.log('signedTransaction:', signedTransaction);

추가 - 가스 설정 (옵션: EIP-1559 Type-2)

네트워크 상황에 따라 EIP-1559(Type-2)를 사용해 가스비를 동적으로 설정할 수 있습니다.

// EIP-1559 수수료 추정
const { maxFeePerGas, maxPriorityFeePerGas } =
  await publicClient.estimateFeesPerGas();

const signedTransaction = await walletClient.signTransaction({
  to: unsignedTransaction.to,
  data: unsignedTransaction.data,
  value: BigInt(unsignedTransaction.value),
  nonce,
  gas,
  maxFeePerGas,
  maxPriorityFeePerGas,
});

Gas limit/fee가 부족하면 트랜잭션이 실패(revert 또는 out-of-gas)할 수 있습니다.


3. 예치 트랜잭션 브로드캐스팅

서명된 트랜잭션을 DSRV API를 통해 네트워크에 브로드캐스트합니다. 브로드캐스팅이 완료되면 트랜잭션 해시가 반환됩니다.

const response = await fetch(
  `https://api.dsrv.com/api/v1/staking/ethereum/hoodi/broadcast`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      signedTransaction,
    }),
  },
);

if (!response.ok) {
  const errorText = await response.text();
  throw new Error(`Broadcast Fail: ${response.status} - ${errorText}`);
}

const broadcastData = await response.json();
const txHash = broadcastData.data.txHash;
console.log('txHash:', txHash);

응답 예시:

{
  "statusCode": 200,
  "data": {
    "txHash": "0x0d5f7ca....",
    "network": "hoodi"
  }
}

4. 브로드캐스팅 확인

반환된 txHash로 트랜잭션이 블록에 포함되었는지(컨펌 여부) 확인합니다. 상태가 confirmed로 변경되면, ETH 스테이킹 예치가 정상적으로 완료된 것입니다.

const response = await fetch(
  `https://api.dsrv.com/api/v1/staking/ethereum/hoodi/transactions`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      txHash,
    }),
  },
);

if (!response.ok) {
  const errorText = await response.text();
  throw new Error(`Transactions Fail: ${response.status} - ${errorText}`);
}

const txData = await response.json();
console.log(txData);

응답 예시 (pending 상태):

{
  "statusCode": 200,
  "data": {
    "result": {
      "txHash": "0x0d5f7caf178.....",
      "network": "hoodi",
      "status": "pending",
      "message": "Transaction is in mempool but not yet included in a block."
    }
  }
}

응답 예시 (confirmed 상태):

{
  "statusCode": 200,
  "data": {
    "result": {
      "txHash": "0x0d5f7caf178.....",
      "network": "hoodi",
      "status": "confirmed",
      "message": "Transaction confirmed in block 12345678",
      "receipt": {
        "blockNumber": 12345678,
        "blockHash": "0x...",
        "transactionHash": "0x0d5f7caf178.....",
        "status": "success",
        "gasUsed": "21000",
        ...
      }
    }
  }
}

트랜잭션 상태값

상태설명
not_found트랜잭션이 mempool에 없거나 삭제됨
pending트랜잭션이 mempool에 있으나 아직 블록에 포함되지 않음
confirmed트랜잭션이 블록에 포함되어 확정됨
timeout트랜잭션 receipt 조회 시간 초과

confirmed 상태일 때만 receipt 필드가 포함됩니다. receipt에는 블록 번호, 가스 사용량 등 상세 정보가 담겨 있습니다.


5. 출금 서명 생성

이 단계에서는 출금 요청에 필요한 서명을 생성합니다. 출금(Voluntary Exit)은 되돌릴 수 없는 작업이므로, 본인 확인을 위해 Withdrawal Address의 개인 키로 서명이 필요합니다. 개인 키는 DSRV에 전달되지 않으며, 클라이언트에서 직접 서명됩니다.

import { privateKeyToAccount } from 'viem/accounts';

const PRIVATE_KEY = '0xYourWithdrawalPrivateKey';
const account = privateKeyToAccount(PRIVATE_KEY);

// 서버로 함께 보낼 customText와 정확히 일치해야 합니다.
const customText = 'I confirm withdrawal request';

const withdrawerSign = await account.signMessage({
  message: customText,
});

console.log('withdrawerSign:', withdrawerSign);
console.log('withdrawerAddress:', account.address);

6. 출금 요청

생성된 서명과 함께 출금 요청을 DSRV API에 전송합니다. API는 Voluntary Exit 메시지를 생성하고 비콘체인에 직접 제출합니다. 요청이 성공하면 밸리데이터는 Exit Queue에 진입하며, 네트워크 상황에 따라 실제 출금까지 시간이 소요될 수 있습니다.

const response = await fetch(
  `https://api.dsrv.com/api/v1/staking/ethereum/hoodi/request-withdraw`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      pubkeys: [PUBKEY],
      withdrawerSign,
      customText,
      withdrawerAddress: WITHDRAW_ADDRESS,
    }),
  },
);

if (!response.ok) {
  const errorText = await response.text();
  throw new Error(`Request Withdraw Fail: ${response.status} - ${errorText}`);
}

const responseData = await response.json();
console.log(responseData);

응답 예시:

{
  "statusCode": 200,
  "data": {
    "result": {
      "exitData": {
        "exit_data": [
          {
            "message": {
              "epoch": "200000",
              "validator_index": "12345"
            },
            "signature": "0xabcd1234...."
          }
        ]
      },
      "beaconSubmission": {
        "successful": [
          {
            "exitData": {
              "message": { "epoch": "200000", "validator_index": "12345" },
              "signature": "0xabcd1234...."
            },
            "success": true
          }
        ],
        "failed": [],
        "totalRequested": 1,
        "totalSuccessful": 1,
        "totalFailed": 0
      }
    }
  }
}

출금 응답 필드 설명

필드타입설명
exitData.exit_dataarray생성된 Voluntary Exit 데이터 배열
exitData.exit_data[].message.epochstringExit이 처리될 epoch
exitData.exit_data[].message.validator_indexstring밸리데이터 인덱스
exitData.exit_data[].signaturestringExit 메시지 서명
beaconSubmission.successfularray비콘체인 제출 성공 목록
beaconSubmission.failedarray비콘체인 제출 실패 목록
beaconSubmission.totalRequestednumber총 요청된 Exit 수
beaconSubmission.totalSuccessfulnumber성공한 Exit 수
beaconSubmission.totalFailednumber실패한 Exit 수

각 단계별 요청·응답 스펙과 추가 옵션에 대한 상세 내용은 API Reference 문서를 참고해주세요.