Skip to content

Commit

Permalink
Merge branch 'evan/gas-specs-2' of github.com:celestiaorg/lazyledger-…
Browse files Browse the repository at this point in the history
…app into evan/gas-specs-2
  • Loading branch information
evan-forbes committed Jul 25, 2023
2 parents 8b13e54 + 2761561 commit 2a4ef9c
Show file tree
Hide file tree
Showing 48 changed files with 1,610 additions and 1,299 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/docker-build-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ jobs:
uses: celestiaorg/.github/.github/workflows/[email protected] # yamllint disable-line rule:line-length
with:
dockerfile: Dockerfile

docker-txsim-build:
permissions:
contents: write
packages: write
uses: celestiaorg/.github/.github/workflows/[email protected]
with:
dockerfile: docker/Dockerfile_txsim
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ test-short:
@go test -mod=readonly ./... -short
.PHONY: test-short

## test-e2e: Run end to end tests via knuu.
test-e2e:
@echo "--> Running e2e tests"
@KNUU_NAMESPACE=celestia-app E2E=true go test -mod=readonly ./test/e2e/... -timeout 30m
.PHONY: test-e2e

## test-race: Run unit tests in race mode.
test-race:
@echo "--> Running tests in race mode"
Expand All @@ -165,6 +171,26 @@ test-coverage:
@export VERSION=$(VERSION); bash -x scripts/test_cover.sh
.PHONY: test-coverage

## txsim-install: Install the tx simulator.
txsim-install:
@echo "--> Installing tx simulator"
@go install -mod=readonly $(BUILD_FLAGS) ./test/cmd/txsim
.PHONY: txsim-install

## txsim-build: Build the tx simulator binary into the ./build directory.
txsim-build:
@echo "--> Building tx simulator"
@cd ./test/cmd/txsim
@mkdir -p build/
@go build $(BUILD_FLAGS) -o build/ ./test/cmd/txsim
@go mod tidy -compat=1.20
.PHONY: txsim-build

## docker-txsim-build: Build the Docker image tx simulator.
txsim-build-docker:
docker build -t ghcr.io/celestiaorg/txsim -f docker/Dockerfile_txsim .
.PHONY: txsim-build-docker

## adr-gen: Download the ADR template from the celestiaorg/.github repo. Ex. `make adr-gen`
adr-gen:
@echo "--> Downloading ADR template"
Expand Down
80 changes: 80 additions & 0 deletions app/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package errors

import (
"errors"
"fmt"
"regexp"
"strconv"

sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

var (
// This is relatively brittle. It would be better if going below the min gas price
// had a specific error type.
regexpMinGasPrice = regexp.MustCompile(`insufficient fees; got: \d+utia required: \d+utia`)
regexpInt = regexp.MustCompile(`[0-9]+`)
)

// ParseInsufficientMinGasPrice checks if the error is due to the gas price being too low.
// Given the previous gas price and gas limit, it returns the new minimum gas price that
// the node should accept.
// If the error is not due to the gas price being too low, it returns 0, nil
func ParseInsufficientMinGasPrice(err error, gasPrice float64, gasLimit uint64) (float64, error) {
// first work out if the error is ErrInsufficientFunds
if err == nil || !sdkerrors.ErrInsufficientFee.Is(err) {
return 0, nil
}

// As there are multiple cases of ErrInsufficientFunds, we need to check the error message
// matches the regexp
substr := regexpMinGasPrice.FindAllString(err.Error(), -1)
if len(substr) != 1 {
return 0, nil
}

// extract the first and second numbers from the error message (got and required)
numbers := regexpInt.FindAllString(substr[0], -1)
if len(numbers) != 2 {
return 0, fmt.Errorf("expected two numbers in error message got %d", len(numbers))
}

// attempt to parse them into float64 values
got, err := strconv.ParseFloat(numbers[0], 64)
if err != nil {
return 0, err
}
required, err := strconv.ParseFloat(numbers[1], 64)
if err != nil {
return 0, err
}

// catch rare condition that required is zero. This should theoretically
// never happen as a min gas price of zero should always be accepted.
if required == 0 {
return 0, errors.New("unexpected case: required gas price is zero (why was an error returned)")
}

// calculate the actual min gas price of the node based on the difference
// between the got and required values. If gas price was zero, we need to use
// the gasLimit to infer this.
if gasPrice == 0 || got == 0 {
if gasLimit == 0 {
return 0, fmt.Errorf("gas limit and gas price cannot be zero")
}
return required / float64(gasLimit), nil
}
return required / got * gasPrice, nil
}

// IsInsufficientMinGasPrice checks if the error is due to the gas price being too low.
func IsInsufficientMinGasPrice(err error) bool {
// first work out if the error is ErrInsufficientFunds
if err == nil || !sdkerrors.ErrInsufficientFee.Is(err) {
return false
}

// As there are multiple cases of ErrInsufficientFunds, we need to check the error message
// matches the regexp
return regexpMinGasPrice.MatchString(err.Error())
}
145 changes: 145 additions & 0 deletions app/errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package errors_test

import (
"fmt"
"testing"

"cosmossdk.io/errors"
"github.com/celestiaorg/celestia-app/app"
apperr "github.com/celestiaorg/celestia-app/app/errors"
"github.com/celestiaorg/celestia-app/pkg/appconsts"
"github.com/celestiaorg/celestia-app/pkg/namespace"
testutil "github.com/celestiaorg/celestia-app/test/util"
blob "github.com/celestiaorg/celestia-app/x/blob/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)

// This will detect any changes to the DeductFeeDecorator which may cause a
// different error message that does not match the regexp.
func TestInsufficientMinGasPriceIntegration(t *testing.T) {
var (
gasLimit uint64 = 1_000_000
feeAmount int64 = 10
gasPrice = float64(feeAmount) / float64(gasLimit)
)
account := "test"
testApp, kr := testutil.SetupTestAppWithGenesisValSet(app.DefaultConsensusParams(), account)
minGasPrice, err := sdk.ParseDecCoins(fmt.Sprintf("%v%s", appconsts.DefaultMinGasPrice, app.BondDenom))
require.NoError(t, err)
ctx := testApp.NewContext(true, tmproto.Header{}).WithMinGasPrices(minGasPrice)
signer := blob.NewKeyringSigner(kr, account, testutil.ChainID)
builder := signer.NewTxBuilder()
builder.SetGasLimit(gasLimit)
builder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(feeAmount))))

address, err := signer.GetSignerInfo().GetAddress()
require.NoError(t, err)

_, err = sdk.AccAddressFromBech32(address.String())
require.NoError(t, err, address)

b, err := blob.NewBlob(namespace.RandomNamespace(), []byte("hello world"), 0)
require.NoError(t, err)

pfb, err := blob.NewMsgPayForBlobs(address.String(), b)
require.NoError(t, err, address)

tx, err := signer.BuildSignedTx(builder, pfb)
require.NoError(t, err)

decorator := ante.NewDeductFeeDecorator(testApp.AccountKeeper, testApp.BankKeeper, testApp.FeeGrantKeeper, nil)
anteHandler := sdk.ChainAnteDecorators(decorator)

_, err = anteHandler(ctx, tx, false)
require.True(t, apperr.IsInsufficientMinGasPrice(err))
actualGasPrice, err := apperr.ParseInsufficientMinGasPrice(err, gasPrice, gasLimit)
require.NoError(t, err)
require.Equal(t, appconsts.DefaultMinGasPrice, actualGasPrice, err)
}

func TestInsufficientMinGasPriceTable(t *testing.T) {
testCases := []struct {
name string
err error
inputGasPrice float64
inputGasLimit uint64
isInsufficientMinGasPriceErr bool
expectParsingError bool
expectedGasPrice float64
}{
{
name: "nil error",
err: nil,
isInsufficientMinGasPriceErr: false,
},
{
name: "not insufficient fee error",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "not enough gas to pay for blobs (minimum: 1000000, got: 100000)"),
isInsufficientMinGasPriceErr: false,
},
{
name: "not insufficient fee error 2",
err: errors.Wrap(sdkerrors.ErrInsufficientFunds, "not enough gas to pay for blobs (got: 1000000, required: 100000)"),
isInsufficientMinGasPriceErr: false,
},
{
name: "insufficient fee error",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 10utia required: 100utia"),
inputGasPrice: 0.01,
expectedGasPrice: 0.1,
isInsufficientMinGasPriceErr: true,
},
{
name: "insufficient fee error with zero gas price",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 0utia required: 100utia"),
inputGasPrice: 0,
inputGasLimit: 100,
expectedGasPrice: 1,
isInsufficientMinGasPriceErr: true,
},
{
name: "insufficient fee error with zero gas price and zero gas limit",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 0utia required: 100utia"),
inputGasPrice: 0,
inputGasLimit: 0,
isInsufficientMinGasPriceErr: true,
expectParsingError: true,
},
{
name: "incorrectly formatted error",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 0uatom required: 100uatom"),
isInsufficientMinGasPriceErr: false,
},
{
name: "error with zero required gas price",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 10utia required: 0utia"),
isInsufficientMinGasPriceErr: true,
expectParsingError: true,
},
{
name: "error with extra wrapping",
err: errors.Wrap(errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 10utia required: 100utia"), "extra wrapping"),
inputGasPrice: 0.01,
expectedGasPrice: 0.1,
isInsufficientMinGasPriceErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.isInsufficientMinGasPriceErr, apperr.IsInsufficientMinGasPrice(tc.err))
actualGasPrice, err := apperr.ParseInsufficientMinGasPrice(tc.err, tc.inputGasPrice, tc.inputGasLimit)
if tc.expectParsingError {
require.Error(t, err)
require.Zero(t, actualGasPrice)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedGasPrice, actualGasPrice)
}
})
}
}
49 changes: 49 additions & 0 deletions docker/Dockerfile_txsim
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Stage 1: generate celestia-appd binary
FROM docker.io/golang:1.20.6-alpine3.17 as builder
# hadolint ignore=DL3018
RUN apk update && apk add --no-cache \
gcc \
git \
make \
musl-dev
COPY . /celestia-app
WORKDIR /celestia-app
# we need the celestia-appd build as we might want to create an account
# internally for txsimulation
RUN make build && make txsim-build

# Stage 2: create a minimal image with the binary
FROM docker.io/alpine:3.18.2

# Use UID 10,001 because UIDs below 10,000 are a security risk.
# Ref: https://github.com/hexops/dockerfile/blob/main/README.md#do-not-use-a-uid-below-10000
ARG UID=10001
ARG USER_NAME=celestia

ENV CELESTIA_HOME=/home/${USER_NAME}

# hadolint ignore=DL3018
RUN apk update && apk add --no-cache \
bash \
curl \
jq \
# Creates a user with $UID and $GID=$UID
&& adduser ${USER_NAME} \
-D \
-g ${USER_NAME} \
-h ${CELESTIA_HOME} \
-s /sbin/nologin \
-u ${UID}

# Copy in the celestia-appd binary
COPY --from=builder /celestia-app/build/celestia-appd /bin/celestia-appd
COPY --from=builder /celestia-app/build/txsim /bin/txsim

COPY --chown=${USER_NAME}:${USER_NAME} docker/txsim.sh /opt/entrypoint.sh

USER ${USER_NAME}

# grpc, rpc, api ports
EXPOSE 26657 1317 9090

ENTRYPOINT [ "/bin/bash", "/opt/entrypoint.sh" ]
Loading

0 comments on commit 2a4ef9c

Please sign in to comment.