Commit 58ce71f2 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2251 from cfromknecht/teleportr-api-server

feat: add teleportr API server
parents 916be607 f4f3054a
---
'@eth-optimism/teleportr': patch
---
Add teleportr API server
---
'@eth-optimism/teleportr': patch
---
Restructure Deposit and CompletedTeleport to use struct embeddings
---
'@eth-optimism/teleportr': patch
---
Add LoadInTeleport method to database
---
'@eth-optimism/teleportr': patch
---
Add btree index on deposit.txn_hash and deposit.address
......@@ -13,8 +13,12 @@ DISBURSER_ARTIFACT := ../../packages/contracts/artifacts/contracts/L2/teleportr/
teleportr:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr
teleportr-api:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr-api
clean:
rm teleportr
rm api
test:
go test -v ./...
......@@ -48,6 +52,7 @@ bindings-disburser:
.PHONY: \
teleportr \
teleportr-api \
bindings \
bindings-deposit \
bindings-disburser \
......
package api
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const TeleportrAPINamespace = "teleportr_api"
var (
rpcRequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "rpc_requests_total",
Help: "Count of total client RPC requests.",
})
httpResponseCodesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "http_response_codes_total",
Help: "Count of total HTTP response codes.",
}, []string{
"status_code",
})
httpRequestDurationSumm = promauto.NewSummary(prometheus.SummaryOpts{
Namespace: TeleportrAPINamespace,
Name: "http_request_duration_seconds",
Help: "Summary of HTTP request durations, in seconds.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
})
databaseErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "database_errors_total",
Help: "Count of total database failures.",
}, []string{
"method",
})
rpcErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "rpc_errors_total",
Help: "Count of total L1 rpc failures.",
}, []string{
"method",
})
)
This diff is collapsed.
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/go/teleportr/api"
"github.com/ethereum-optimism/optimism/go/teleportr/flags"
)
var (
GitVersion = ""
GitCommit = ""
GitDate = ""
)
func main() {
// Set up logger with a default INFO level in case we fail to parse flags.
// Otherwise the final critical log won't show what the parsing error was.
log.Root().SetHandler(
log.LvlFilterHandler(
log.LvlInfo,
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
),
)
app := cli.NewApp()
app.Flags = flags.APIFlags
app.Version = fmt.Sprintf("%s-%s-%s", GitVersion, GitCommit, GitDate)
app.Name = "teleportr-api"
app.Usage = "Teleportr API server"
app.Description = "API serving teleportr data"
app.Action = api.Main(GitVersion)
err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}
......@@ -21,17 +21,6 @@ var (
ErrUnknownDeposit = errors.New("unknown deposit")
)
// Deposit represents an event emitted from the TeleportrDeposit contract on L1,
// along with additional info about the tx that generated the event.
type Deposit struct {
ID uint64
TxnHash common.Hash
BlockNumber uint64
BlockTimestamp time.Time
Address common.Address
Amount *big.Int
}
// ConfirmationInfo holds metadata about a tx on either the L1 or L2 chain.
type ConfirmationInfo struct {
TxnHash common.Hash
......@@ -39,15 +28,28 @@ type ConfirmationInfo struct {
BlockTimestamp time.Time
}
// CompletedTeleport represents an L1 deposit that has been disbursed on L2. The
// struct also hold info about the L1 and L2 txns involved.
type CompletedTeleport struct {
ID uint64
Address common.Address
Amount *big.Int
Success bool
Deposit ConfirmationInfo
Disbursement ConfirmationInfo
// Deposit represents an event emitted from the TeleportrDeposit contract on L1,
// along with additional info about the tx that generated the event.
type Deposit struct {
ID uint64
Address common.Address
Amount *big.Int
ConfirmationInfo
}
type Disbursement struct {
Success bool
ConfirmationInfo
}
// Teleport represents the combination of an L1 deposit and its disbursement on
// L2. Disburment will be nil if the L2 disbursement has not occurred.
type Teleport struct {
Deposit
Disbursement *Disbursement
}
const createDepositsTable = `
......@@ -61,6 +63,14 @@ CREATE TABLE IF NOT EXISTS deposits (
);
`
const createDepositTxnHashIndex = `
CREATE INDEX ON deposits (txn_hash)
`
const createDepositAddressIndex = `
CREATE INDEX ON deposits (address)
`
const createDisbursementsTable = `
CREATE TABLE IF NOT EXISTS disbursements (
id INT8 NOT NULL PRIMARY KEY REFERENCES deposits(id),
......@@ -89,6 +99,8 @@ CREATE TABLE IF NOT EXISTS pending_txs (
var migrations = []string{
createDepositsTable,
createDepositTxnHashIndex,
createDepositAddressIndex,
createDisbursementsTable,
lastProcessedBlockTable,
pendingTxTable,
......@@ -371,6 +383,69 @@ func (d *Database) UpsertDisbursement(
return nil
}
const loadTeleportByDepositHashQuery = `
SELECT
dep.id, dep.address, dep.amount, dis.success,
dep.txn_hash, dep.block_number, dep.block_timestamp,
dis.txn_hash, dis.block_number, dis.block_timestamp
FROM deposits AS dep
LEFT JOIN disbursements AS dis
ON dep.id = dis.id AND dep.txn_hash = $1
LIMIT 1
`
func (d *Database) LoadTeleportByDepositHash(
txHash common.Hash,
) (*Teleport, error) {
row := d.conn.QueryRow(loadTeleportByDepositHashQuery, txHash.String())
teleport, err := scanTeleport(row)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
return &teleport, nil
}
const loadTeleportsByAddressQuery = `
SELECT
dep.id, dep.address, dep.amount, dis.success,
dep.txn_hash, dep.block_number, dep.block_timestamp,
dis.txn_hash, dis.block_number, dis.block_timestamp
FROM deposits AS dep
LEFT JOIN disbursements AS dis
ON dep.id = dis.id AND dep.address = $1
ORDER BY dep.block_timestamp DESC, dep.id DESC
LIMIT 100
`
func (d *Database) LoadTeleportsByAddress(
addr common.Address,
) ([]Teleport, error) {
rows, err := d.conn.Query(loadTeleportsByAddressQuery, addr.String())
if err != nil {
return nil, err
}
defer rows.Close()
var teleports []Teleport
for rows.Next() {
teleport, err := scanTeleport(rows)
if err != nil {
return nil, err
}
teleports = append(teleports, teleport)
}
if err := rows.Err(); err != nil {
return nil, err
}
return teleports, nil
}
const completedTeleportsQuery = `
SELECT
dep.id, dep.address, dep.amount, dis.success,
......@@ -383,46 +458,19 @@ ORDER BY id DESC
// CompletedTeleports returns the set of all deposits that have also been
// disbursed.
func (d *Database) CompletedTeleports() ([]CompletedTeleport, error) {
func (d *Database) CompletedTeleports() ([]Teleport, error) {
rows, err := d.conn.Query(completedTeleportsQuery)
if err != nil {
return nil, err
}
defer rows.Close()
var teleports []CompletedTeleport
var teleports []Teleport
for rows.Next() {
var teleport CompletedTeleport
var addressStr string
var amountStr string
var depTxnHashStr string
var disTxnHashStr string
err = rows.Scan(
&teleport.ID,
&addressStr,
&amountStr,
&teleport.Success,
&depTxnHashStr,
&teleport.Deposit.BlockNumber,
&teleport.Deposit.BlockTimestamp,
&disTxnHashStr,
&teleport.Disbursement.BlockNumber,
&teleport.Disbursement.BlockTimestamp,
)
teleport, err := scanTeleport(rows)
if err != nil {
return nil, err
}
amount, ok := new(big.Int).SetString(amountStr, 10)
if !ok {
return nil, fmt.Errorf("unable to parse amount %v", amount)
}
teleport.Address = common.HexToAddress(addressStr)
teleport.Amount = amount
teleport.Deposit.TxnHash = common.HexToHash(depTxnHashStr)
teleport.Deposit.BlockTimestamp = teleport.Deposit.BlockTimestamp.Local()
teleport.Disbursement.TxnHash = common.HexToHash(disTxnHashStr)
teleport.Disbursement.BlockTimestamp = teleport.Disbursement.BlockTimestamp.Local()
teleports = append(teleports, teleport)
}
if err := rows.Err(); err != nil {
......@@ -432,6 +480,63 @@ func (d *Database) CompletedTeleports() ([]CompletedTeleport, error) {
return teleports, nil
}
type Scanner interface {
Scan(...interface{}) error
}
func scanTeleport(scanner Scanner) (Teleport, error) {
var teleport Teleport
var addressStr string
var amountStr string
var depTxnHashStr string
var disTxnHashStr *string
var disBlockNumber *uint64
var disBlockTimestamp *time.Time
var success *bool
err := scanner.Scan(
&teleport.ID,
&addressStr,
&amountStr,
&success,
&depTxnHashStr,
&teleport.Deposit.BlockNumber,
&teleport.Deposit.BlockTimestamp,
&disTxnHashStr,
&disBlockNumber,
&disBlockTimestamp,
)
if err != nil {
return Teleport{}, err
}
amount, ok := new(big.Int).SetString(amountStr, 10)
if !ok {
return Teleport{}, fmt.Errorf("unable to parse amount %v", amount)
}
teleport.Address = common.HexToAddress(addressStr)
teleport.Amount = amount
teleport.Deposit.TxnHash = common.HexToHash(depTxnHashStr)
teleport.Deposit.BlockTimestamp = teleport.Deposit.BlockTimestamp.Local()
hasDisbursement := success != nil &&
disTxnHashStr != nil &&
disBlockNumber != nil &&
disBlockTimestamp != nil
if hasDisbursement {
teleport.Disbursement = &Disbursement{
ConfirmationInfo: ConfirmationInfo{
TxnHash: common.HexToHash(*disTxnHashStr),
BlockNumber: *disBlockNumber,
BlockTimestamp: disBlockTimestamp.Local(),
},
Success: *success,
}
}
return teleport, nil
}
// PendingTx encapsulates the metadata stored about published disbursement txs.
type PendingTx struct {
// Txhash is the tx hash of the disbursement tx.
......
......@@ -91,12 +91,14 @@ func TestUpsertDeposits(t *testing.T) {
defer d.Close()
deposit1 := db.Deposit{
ID: 1,
TxnHash: common.HexToHash("0xff01"),
BlockNumber: 1,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa01"),
Amount: big.NewInt(1),
ID: 1,
Address: common.HexToAddress("0xaa01"),
Amount: big.NewInt(1),
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: common.HexToHash("0xff01"),
BlockNumber: 1,
BlockTimestamp: testTimestamp,
},
}
err := d.UpsertDeposits([]db.Deposit{deposit1}, 0)
......@@ -107,12 +109,14 @@ func TestUpsertDeposits(t *testing.T) {
require.Equal(t, deposits, []db.Deposit{deposit1})
deposit2 := db.Deposit{
ID: 1,
TxnHash: common.HexToHash("0xff02"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa02"),
Amount: big.NewInt(2),
ID: 1,
Address: common.HexToAddress("0xaa02"),
Amount: big.NewInt(2),
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: common.HexToHash("0xff02"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
},
}
err = d.UpsertDeposits([]db.Deposit{deposit2}, 0)
......@@ -160,12 +164,14 @@ func TestUpsertDepositsRecordsLastProcessedBlock(t *testing.T) {
// Insert real deposit in block 3 with last processed at 4.
deposit := db.Deposit{
ID: 0,
TxnHash: common.HexToHash("0xff03"),
BlockNumber: 3,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa03"),
Amount: big.NewInt(3),
ID: 0,
Address: common.HexToAddress("0xaa03"),
Amount: big.NewInt(3),
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: common.HexToHash("0xff03"),
BlockNumber: 3,
BlockTimestamp: testTimestamp,
},
}
err = d.UpsertDeposits([]db.Deposit{deposit}, 4)
require.Nil(t, err)
......@@ -190,28 +196,34 @@ func TestConfirmedDeposits(t *testing.T) {
require.Equal(t, int(0), len(deposits))
deposit1 := db.Deposit{
ID: 1,
TxnHash: common.HexToHash("0xff01"),
BlockNumber: 1,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa01"),
Amount: big.NewInt(1),
ID: 1,
Address: common.HexToAddress("0xaa01"),
Amount: big.NewInt(1),
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: common.HexToHash("0xff01"),
BlockNumber: 1,
BlockTimestamp: testTimestamp,
},
}
deposit2 := db.Deposit{
ID: 2,
TxnHash: common.HexToHash("0xff21"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa21"),
Amount: big.NewInt(2),
ID: 2,
Address: common.HexToAddress("0xaa21"),
Amount: big.NewInt(2),
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: common.HexToHash("0xff21"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
},
}
deposit3 := db.Deposit{
ID: 3,
TxnHash: common.HexToHash("0xff22"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa22"),
Amount: big.NewInt(2),
ID: 3,
Address: common.HexToAddress("0xaa22"),
Amount: big.NewInt(2),
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: common.HexToHash("0xff22"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
},
}
err = d.UpsertDeposits([]db.Deposit{
......@@ -269,12 +281,14 @@ func TestUpsertDisbursement(t *testing.T) {
// Now, insert a real deposit that we will disburse.
err = d.UpsertDeposits([]db.Deposit{
{
ID: 1,
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
Address: address,
Amount: amount,
ID: 1,
Address: address,
Amount: amount,
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
},
},
}, 0)
require.Nil(t, err)
......@@ -287,21 +301,25 @@ func TestUpsertDisbursement(t *testing.T) {
)
require.Nil(t, err)
expTeleports := []db.CompletedTeleport{
expTeleports := []db.Teleport{
{
ID: 1,
Address: address,
Amount: amount,
Success: false,
Deposit: db.ConfirmationInfo{
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
Deposit: db.Deposit{
ID: 1,
Address: address,
Amount: amount,
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
},
},
Disbursement: db.ConfirmationInfo{
TxnHash: tempDisTxnHash,
BlockNumber: tempDisBlockNumber,
BlockTimestamp: testTimestamp,
Disbursement: &db.Disbursement{
Success: false,
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: tempDisTxnHash,
BlockNumber: tempDisBlockNumber,
BlockTimestamp: testTimestamp,
},
},
},
}
......@@ -316,21 +334,25 @@ func TestUpsertDisbursement(t *testing.T) {
err = d.UpsertDisbursement(1, disTxnHash, disBlockNumber, testTimestamp, true)
require.Nil(t, err)
expTeleports = []db.CompletedTeleport{
expTeleports = []db.Teleport{
{
ID: 1,
Address: address,
Amount: amount,
Success: true,
Deposit: db.ConfirmationInfo{
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
Deposit: db.Deposit{
ID: 1,
Address: address,
Amount: amount,
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
},
},
Disbursement: db.ConfirmationInfo{
TxnHash: disTxnHash,
BlockNumber: disBlockNumber,
BlockTimestamp: testTimestamp,
Disbursement: &db.Disbursement{
Success: true,
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: disTxnHash,
BlockNumber: disBlockNumber,
BlockTimestamp: testTimestamp,
},
},
},
}
......
......@@ -509,12 +509,14 @@ func (d *Driver) ingestDeposits(
}
deposits = append(deposits, db.Deposit{
ID: event.DepositId.Uint64(),
TxnHash: event.Raw.TxHash,
BlockNumber: event.Raw.BlockNumber,
BlockTimestamp: time.Unix(int64(header.Time), 0),
Address: event.Emitter,
Amount: event.Amount,
ID: event.DepositId.Uint64(),
Address: event.Emitter,
Amount: event.Amount,
ConfirmationInfo: db.ConfirmationInfo{
TxnHash: event.Raw.TxHash,
BlockNumber: event.Raw.BlockNumber,
BlockTimestamp: time.Unix(int64(header.Time), 0),
},
})
}
err = events.Error()
......
package flags
import (
"fmt"
"strings"
"github.com/urfave/cli"
)
func prefixAPIEnvVar(name string) string {
return fmt.Sprintf("TELEPORTR_API_%s", strings.ToUpper(name))
}
var (
APIHostnameFlag = cli.StringFlag{
Name: "hostname",
Usage: "The hostname of the API server",
Required: true,
EnvVar: prefixAPIEnvVar("HOSTNAME"),
}
APIPortFlag = cli.StringFlag{
Name: "port",
Usage: "The hostname of the API server",
Required: true,
EnvVar: prefixAPIEnvVar("PORT"),
}
APIL1EthRpcFlag = cli.StringFlag{
Name: "l1-eth-rpc",
Usage: "The endpoint for the L1 ETH provider",
Required: true,
EnvVar: prefixAPIEnvVar("L1_ETH_RPC"),
}
APIDepositAddressFlag = cli.StringFlag{
Name: "deposit-address",
Usage: "Address of the TeleportrDeposit contract",
Required: true,
EnvVar: prefixAPIEnvVar("DEPOSIT_ADDRESS"),
}
APINumConfirmationsFlag = cli.StringFlag{
Name: "num-confirmations",
Usage: "Number of confirmations required until deposits are " +
"considered confirmed",
Required: true,
EnvVar: prefixAPIEnvVar("NUM_CONFIRMATIONS"),
}
APIPostgresHostFlag = cli.StringFlag{
Name: "postgres-host",
Usage: "Host of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_HOST"),
}
APIPostgresPortFlag = cli.Uint64Flag{
Name: "postgres-port",
Usage: "Port of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_PORT"),
}
APIPostgresUserFlag = cli.StringFlag{
Name: "postgres-user",
Usage: "Username of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_USER"),
}
APIPostgresPasswordFlag = cli.StringFlag{
Name: "postgres-password",
Usage: "Password of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_PASSWORD"),
}
APIPostgresDBNameFlag = cli.StringFlag{
Name: "postgres-db-name",
Usage: "Database name of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_DB_NAME"),
}
APIPostgresEnableSSLFlag = cli.BoolFlag{
Name: "postgres-enable-ssl",
Usage: "Whether or not to enable SSL on connections to " +
"teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_ENABLE_SSL"),
}
)
var APIFlags = []cli.Flag{
APIHostnameFlag,
APIPortFlag,
APIL1EthRpcFlag,
APIDepositAddressFlag,
APINumConfirmationsFlag,
APIPostgresHostFlag,
APIPostgresPortFlag,
APIPostgresUserFlag,
APIPostgresPasswordFlag,
APIPostgresDBNameFlag,
APIPostgresEnableSSLFlag,
}
......@@ -6,7 +6,10 @@ require (
github.com/ethereum-optimism/optimism/go/bss-core v0.0.0-20220218171106-67a0414d7606
github.com/ethereum/go-ethereum v1.10.15
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.4
github.com/prometheus/client_golang v1.11.0
github.com/rs/cors v1.7.0
github.com/stretchr/testify v1.7.0
github.com/urfave/cli v1.22.5
)
......@@ -39,7 +42,6 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
......
......@@ -265,6 +265,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment