본문 바로가기
블록체인 backEnd/web3j

maxFeePerGas, maxPriorityFeePerGas 추정기

by gun_poo 2023. 3. 18.

메인넷을 지원하는 gas tracker api들은 여러 종류가 있더라. 이 api를 써서 사용하면 될거 같은데.

테스트넷을 지원하는 api는 찾기가 힘들다 

그래서 만들어보려한다.

 

트랜잭션이 처리되는 원리를 생각해보면 

유저가 트랜잭션을 전송하고 채굴자가 채굴을 해가야하는데 채굴 우선순위는 최종 가스프라이스가 높은 순이다.

그렇다는 말은 팬딩 트랜잭션을 봐야한단 말이다.

팬딩 트랜잭션들을 조회하여 그들의 maxFeePerGas, maxPriorityFeePerGas 값을 가져와야겠다.

팬딩 트랜잭션을 조회하는 방법은 두가지 정도가 존재한다 

 

1번: JSON-RPC 요청을 직접 사용하여 팬딩 트랜잭션 정보를 얻는 방법

2번: 외부 API를 사용하여 팬딩 트랜잭션 정보를 얻는법

 

네트워크 전반 사용률 확인

 

최근 블록들의 가스 사용량(gasUsed), gasLimit를 비교한다.

이더리움에서 트랜잭션을 처리하기 위해 가스가 발생하는데 가스 사용률은 특정 블록에서 사용된 가스 양과 블록의 최대 가스한도 사이의 비율을 의미한다. 네트워크 활동 수준이 높을수록 많은 트랜잭션이 처리되어야 하며 블록당 가스 사용량이 늘어난다. 따라서 가스 사용량이 높으면 네트워크 수준이 높다고 간주가능하다.

이에 대한 함수를 만들어보자

// 네트워크 사용률을 계산하는 함수
public double getNetworkUtilization(Web3j web3j, int blockCount) throws Exception {
    double totalUtilization = 0; // 전체 사용률 초기화

    // 최신 블록 가져오기
    EthBlock.Block latestBlock = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).send().getBlock();
    BigInteger latestBlockNumber = latestBlock.getNumber();

    // 주어진 블록 개수만큼 반복하면서 각 블록의 사용률 계산
    for (int i = 0; i < blockCount; i++) {
        // 현재 블록 번호에서 i만큼 뺀 블록 가져오기
        EthBlock.Block block = web3j.ethGetBlockByNumber(DefaultBlockParameter.valueOf(latestBlockNumber.subtract(BigInteger.valueOf(i))), false).send().getBlock();
        
        // 블록의 가스 사용량 및 가스 제한 가져오기
        BigInteger gasUsed = block.getGasUsed();
        BigInteger gasLimit = block.getGasLimit();

        // 블록의 사용률 계산 (가스 사용량 / 가스 제한)
        double utilization = gasUsed.doubleValue() / gasLimit.doubleValue();

        // 전체 사용률에 블록의 사용률 더하기
        totalUtilization += utilization;
    }

    // 전체 사용률을 블록 개수로 나누어 평균 사용률 반환
    return totalUtilization / blockCount;
}

 

네트워크 사용률을 뽑아 내다 보니 문득 생각이 들었다. 

굳이 사용률을 뽑지 않고 팬딩 상태인 트랜잭션들을 조회하고 그 값들의 데이터를 뽑아 이상치 제거 및 클러스터링을 거치고

분류를 통해 세분화하고 분류당 최적의 값을 지정하면 되지않을까?

 

우선 팬딩 상태인 트랜잭션을 받아오고 필요한 값들을 받아오는 작업부터 시작해보자

 

팬딩 트랜잭션 조회

private static List<Transaction> getPendingTransactions(Web3j web3j) throws Exception {
    EthBlock pendingBlock = web3j.ethGetBlockByNumber(DefaultBlockParameterName.PENDING, true).send();
    if (pendingBlock.hasError()) {
        throw new Exception("블록 정보를 가져오는데 실패했습니다: " + pendingBlock.getError().getMessage());
    }
    List<Transaction> pendingTransactions = pendingBlock.getBlock().getTransactions().stream()
            .map(transactionResult -> (Transaction) transactionResult.get())
            .filter(transaction -> transaction.getMaxFeePerGas() != null && transaction.getMaxPriorityFeePerGas() != null)
            .collect(Collectors.toList());
    return pendingTransactions;
}

web3j로 팬딩트랜잭션을 가져오는 방법이다. 

web3j.ethGetBlockByNumber(DefaultBlockParameterName.PENDING, true).send()를 호출하여 대기중인 블록정보를 가져온다. 그리고 pendingBlock.getBlock().getTransactions()을 사용하여 대기중인 블록의 트랜잭션 목록을 가져온다.

그리고 java stream을 이용해 트랜잭션 목록에서 각 트랜잭션을 추출하고 maxFeePerGas, maxPriorityFeePerGas가스 둘다 null이 아닌 트랜잭션만 필터링한다. 그리고 필터링된 트랜잭션 목록을 반환한다.

 

maxFeePerGas, maxPriorityFeePerGas 추정

평균값 추정, 알고리즘 추정

우선 이 두가지로 분류하였다. 

평균값 추정

public static BigDecimal[] calculateAverageFees(List<Transaction> transactions) {
    BigInteger totalMaxFee = BigInteger.ZERO;
    BigInteger totalPriorityFee = BigInteger.ZERO;
    int count = transactions.size();

    if (count == 0) {
        System.out.println("팬딩 상태인 트랜잭션이 없습니다.");
        BigDecimal averageMaxFee = BigDecimal.valueOf(4).multiply(BigDecimal.valueOf(1000000000));
        BigDecimal averagePriorityFee = BigDecimal.valueOf(2).multiply(BigDecimal.valueOf(1000000000));
        return new BigDecimal[]{averagePriorityFee, averageMaxFee};
    }

    for (Transaction transaction : transactions) {
        String maxFee = transaction.getMaxFeePerGas();
        byte[] maxFeeBytes = Numeric.hexStringToByteArray(maxFee);
        BigInteger maxFeeBigInteger = new BigInteger(1, maxFeeBytes);

        String maxPriorityFee = transaction.getMaxPriorityFeePerGas();
        byte[] maxPriorityFeeBytes = Numeric.hexStringToByteArray(maxPriorityFee);
        BigInteger maxPriorityFeeBigInteger = new BigInteger(1, maxPriorityFeeBytes);
        totalMaxFee = totalMaxFee.add(maxFeeBigInteger);
        totalPriorityFee = totalPriorityFee.add(maxPriorityFeeBigInteger);
    }

    BigDecimal averageMaxFee = new BigDecimal(totalMaxFee).divide(new BigDecimal(count), 0, RoundingMode.HALF_UP);
    BigDecimal averagePriorityFee = new BigDecimal(totalPriorityFee).divide(new BigDecimal(count), 0, RoundingMode.HALF_UP);
    System.out.println("averageMaxFee: " + averageMaxFee);
    System.out.println("averagePriorityFee: " + averagePriorityFee);
    return new BigDecimal[]{averageMaxFee, averagePriorityFee};
}

불러온 트랜잭션 리스트의 카운트를 구해놓는다.

만약 빈값을 반환하면 2, 4 gwei를 반환하게 만들어둔다.

트랜잭션 데이터 안에 필요한 값들을 불러오고 각각 모두 더해서 카운트 만큼 나눠준다.

그리고 반환해준다.

이는 평균값 추정 공식이다. 

이 공식의 문제점은 무엇일까. 팬딩 상태의 모든 트랜잭션이 성공적으로 클리어되는것이 아니다. 

테스트 서버이기 때문에 개성이 강한 값들을 집어넣어 날리는 사람이 많다. 즉 데이터의 노이즈가 심하다는 뜻이다. 

노이즈 필터링이 필요한 부분인데 이는 추후 보완하는 것으로 하자.

그리고 maxFee는 priorityFee + baseFee 보다 작으면 안된다 라는 부분도 추가해줘야하며

maxFee는 발신자의 잔액보다 작아야한다. 라는 부분도 추가해야한다

 

알고리즘 추정

우선 마구잡이로 날라오는 트랜잭션들 사이 노이즈 값은 빼고 적절한 수치를 골라내어 최적값 계산을 해주어야한다.

이에 이상치를 제거하는 메소드를 추가하고, 데이터를 클러스터링해주고, 최적값을 계산해준다.

 

이상치 제거 : 사분위수 사용(데이터 전처리)

사분위수란 전체 데이터를 4등분하는 지점을 말한다.

제 1사분위수 : 하위 25%

제 2사분위수 : 데이터의 중앙값

제 3사분위수 : 하위 75%

사분위수는 데이터의 분포와 중심 경향을 파악하는데 도움을 준다.

제1사분위수와 제3사분위수 사이의 거리는 사분위수 범위(IQR)라고 하며 데이터의 퍼진 정도를 나타냄

 

// 이상치를 제거하는 메소드
public List<Double> removeOutliers(List<Double> data) {
    // List<Double>을 double[]로 변환
    double[] dataArray = data.stream().mapToDouble(Double::doubleValue).toArray();

    // 제 1사분위수(Q1) 및 제 3사분위수(Q3) 계산 하위 25%, 75%
    double q1 = new Percentile().evaluate(dataArray, 25);
    double q3 = new Percentile().evaluate(dataArray, 75);
    
    //IQR은 데이터의 중간 50% 범위 이를 통해 데이터의 분포와 이상치 판단
    double iqr = q3 - q1;
    
    //IQR을 사용하여 이상치의 하한값과 상한값을 계산 
    // 하한값 계산 : 제 1사분위수에서 1.5*IQR을 뺀값. 하위 범위에서 이상치 구별
    double lowerBound = q1 - 1.5 * iqr;
    // 상한값 계산 : 제 3사분위수에서 1.5*IQR을 뺀값. 상위 범위에서 이상치 구별
    double upperBound = q3 + 1.5 * iqr;

    // 이상치를 제거한 데이터 반환
    return data.stream()
            .filter(value -> value >= lowerBound && value <= upperBound)
            .collect(Collectors.toList());
}

이상치 판별의 엄격성을 조절하기 위해 사용되는 1.5의 값은 상수이며 이상치 판별의 엄격성을 결정하는 값이다.

 

상수 값이 커질 수록 이상치 판별의 엄격성이 낮아지고 상수 값이 낮아질수록 이상치 판별의 엄격성이 높아진다.

 

상수값을 조절하여 데이터를 뽑아볼 필요가 있다.

 

데이터 클러스터링

 

클러스터링 유사한 특성을 가진 데이터 객체들을 그룹화 하는 과정.

KMeansPlusPlusClusterer : k 평균 클러스터링 알고리즘의 변형

초기 클러스터 중심점을 선택하는 방법에 k-means++ 초기화 방식 사용

1. 데이터 포인트 중 무작위 선택, 1 클러스터중심점 설정

2. 각 데이터 포인트와 가장 가까운 클러스터 중심점 사이의 거리를 계산

3. 거리에 비례한 확률로 다음 클러스터 중심점 선택, 먼 거리 데이터 포인트가새로운 중심점으로 선택될 확률 높음

4. k개 클러스터 중심점이 모두 선택 될 때 까지 2-3 반복

public List<Double> calculateOptimalValues(List<CentroidCluster<DoublePoint>> clusters) {
    List<Double> optimalValues = new ArrayList<>();
    for (Cluster<DoublePoint> cluster : clusters) {
        List<Double> values = cluster.getPoints().stream()
                .map(DoublePoint::getPoint)
                .map(point -> point[0])
                .collect(Collectors.toList());
        if (values.isEmpty()) {
            optimalValues.add(0.0);
        } else if (values.size() == 1) {
            optimalValues.add(values.get(0));
        } else {
            double median = calculateMedian(values);
            optimalValues.add(median);
        }
    }
    return optimalValues;
}

우선 클러스터의 개수를 3으로 설정한다(가스비 별 속도를 구분해주기 위해서이다 ex: 평균, 빠름, 매우빠름)

변환된 데이터의 개수가 클러스터의 개수보다 적다면 클러스터링할 데이터가 충분하지 않다고 판별하고 빈 리스트를 반환한다.

kmeansplusplusclusterer 객체를 생성한다. 이 객체는 k평균++ 클러스터링 알고리즘을 구현하고 클러스터의 개수, 최대 반복횟수, 거리측정방식을 인자로 받는다. 

이 메소드가 끝나게 되면 각 클러스터의 중심점과 포함된 데이터 포인트를 얻을 수 있다. 이를 사용해 데이터 분포를 파악하거나 

특정클러스터에 포함된 데이터 특성을 분석 할 수 있다.

 

최적의 가스비 계산

 

앞선 과정을 끝냈다면 이상치를 제거한 각 클러스터별 데이터셋이 모이게 되었을 것이다. 모인 데이터들을 판별하여 각 구간별 최적의 가스비를 구해주어야한다. 

// 중앙값을 계산하는 메소드
private double calculateMedian(List<Double> data) {
    if (data.isEmpty()) {
        return 0;
    }

    Collections.sort(data);
    int size = data.size();
    if (size % 2 == 0) {
        return (data.get(size / 2 - 1) + data.get(size / 2)) / 2;
    } else {
        return data.get(size / 2);
    }
}

입력 데이터가 비어있는 경우 0을 반환한다. 중앙값이 없다고 판별

입력 데이터를 오름차순으로 정렬하고 데이터 갯수를 확인한다.

짝수개인 경우 중앙에 위치한 두 데이터의 평균값을 중앙값으로 반환

홀수개인 경우 중앙에 위치한 데이터를 중앙값으로 반환

중앙값은 평균에 비해 이상치의 영향을 덜 받기 때문에 중심의 경향을 나타내는데 더 적합하다.

 

이렇게 세팅을 끝내고 

테스트넷의 팬딩 트랜잭션의 표본이 충분치 않아서 빈 객체를 반환하게 될 수도 있기때문에 

최소 중간 최고 중 0값을 반환하는 값이 있다면 내가 지정해준 값들로 대체하게 만들었다.

 

트랜잭션을 날려보자!!

 

List<BigInteger> values = calculateFees();
BigInteger minPriorityFee = values.get(0);
BigInteger medianPriorityFee = values.get(1);
BigInteger maxPriorityFee = values.get(2);
BigInteger minMaxFee = values.get(3);
BigInteger medianMaxFee = values.get(4);
BigInteger maxMaxFee = values.get(5);

값을 받아와서 

최고치로 설정해주고 날려보자

maxFees: [1.500000014E9, 1.500000014E9, 1.500000014E9, 6.009756424E9]
priorityFees: [1.5E9, 1.5E9, 1.5E9, 1.5E9]
filteredMaxFees: [1.500000014E9, 1.500000014E9, 1.500000014E9, 6.009756424E9]
filteredPriorityFees: [1.5E9, 1.5E9, 1.5E9, 1.5E9]
Optimal max fees: [6009756424, 1500000014, 0]
Optimal priority fees: [1500000000, 0, 0]
최소 maxFee: 1500000014
중간 maxFee: 3754878219
최고 maxFee: 6009756424
최소 priorityFee: 1500000000
중간 priorityFee: 1500000000
최고 priorityFee: 1500000000

짜란 

이렇게 해서 자동추정을 하여 트랜잭션이 성공적으로 처리되었다 

 

'블록체인 backEnd > web3j' 카테고리의 다른 글

transaction 모듈화  (0) 2023.03.15
가스에 대하여  (1) 2023.03.15
java web3j 컴파일 및 wrappers  (0) 2023.01.25
mac os web3j 설치하기  (0) 2023.01.25

댓글