Skip to content

Commit ee3f3c6

Browse files
committed
core/vm: reject duplicate bridge validators at Mendel
1 parent df92365 commit ee3f3c6

6 files changed

Lines changed: 339 additions & 16 deletions

File tree

core/vm/contracts.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -350,13 +350,21 @@ var PrecompiledContractsOsaka = PrecompiledContracts{
350350
common.BytesToAddress([]byte{0x1, 0x00}): &p256Verify{eip7951: true},
351351
}
352352

353+
var PrecompiledContractsMendel = func() PrecompiledContracts {
354+
precompiles := maps.Clone(PrecompiledContractsOsaka)
355+
precompiles[common.BytesToAddress([]byte{0x66})] = &blsSignatureVerifyMendel{}
356+
precompiles[common.BytesToAddress([]byte{0x67})] = &cometBFTLightBlockValidateHertzMendel{}
357+
return precompiles
358+
}()
359+
353360
// PrecompiledContractsP256Verify contains the precompiled Ethereum
354361
// contract specified in EIP-7212. This is exported for testing purposes.
355362
var PrecompiledContractsP256Verify = PrecompiledContracts{
356363
common.BytesToAddress([]byte{0x1, 0x00}): &p256Verify{},
357364
}
358365

359366
var (
367+
PrecompiledAddressesMendel []common.Address
360368
PrecompiledAddressesOsaka []common.Address
361369
PrecompiledAddressesPrague []common.Address
362370
PrecompiledAddressesHaber []common.Address
@@ -417,6 +425,9 @@ func init() {
417425
for k := range PrecompiledContractsPrague {
418426
PrecompiledAddressesPrague = append(PrecompiledAddressesPrague, k)
419427
}
428+
for k := range PrecompiledContractsMendel {
429+
PrecompiledAddressesMendel = append(PrecompiledAddressesMendel, k)
430+
}
420431
for k := range PrecompiledContractsOsaka {
421432
PrecompiledAddressesOsaka = append(PrecompiledAddressesOsaka, k)
422433
}
@@ -426,6 +437,8 @@ func activePrecompiledContracts(rules params.Rules) PrecompiledContracts {
426437
switch {
427438
case rules.IsVerkle:
428439
return PrecompiledContractsVerkle
440+
case rules.IsMendel:
441+
return PrecompiledContractsMendel
429442
case rules.IsOsaka:
430443
return PrecompiledContractsOsaka
431444
case rules.IsPrague:
@@ -467,6 +480,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts {
467480
// ActivePrecompiles returns the precompile addresses enabled with the current configuration.
468481
func ActivePrecompiles(rules params.Rules) []common.Address {
469482
switch {
483+
case rules.IsMendel:
484+
return PrecompiledAddressesMendel
470485
case rules.IsOsaka:
471486
return PrecompiledAddressesOsaka
472487
case rules.IsPrague:
@@ -1602,6 +1617,10 @@ func (c *bls12381MapG2) Name() string {
16021617
// blsSignatureVerify implements bls signature verification precompile.
16031618
type blsSignatureVerify struct{}
16041619

1620+
type blsSignatureVerifyMendel struct {
1621+
blsSignatureVerify
1622+
}
1623+
16051624
const (
16061625
msgHashLength = uint64(32)
16071626
signatureLength = uint64(96)
@@ -1620,16 +1639,13 @@ func (c *blsSignatureVerify) RequiredGas(input []byte) uint64 {
16201639
return params.BlsSignatureVerifyBaseGas + pubKeyNumber*params.BlsSignatureVerifyPerKeyGas
16211640
}
16221641

1623-
// Run input:
1624-
// msg | signature | [{bls pubkey}] |
1625-
// 32 bytes | 96 bytes | [{48 bytes}] |
1626-
func (c *blsSignatureVerify) Run(input []byte) ([]byte, error) {
1642+
func parseBlsSignatureVerifyInput(input []byte, requireUniquePubKeys bool) ([32]byte, bls.Signature, []bls.PublicKey, error) {
16271643
msgAndSigLength := msgHashLength + signatureLength
16281644
inputLen := uint64(len(input))
16291645
if inputLen <= msgAndSigLength ||
16301646
(inputLen-msgAndSigLength)%singleBlsPubkeyLength != 0 {
16311647
log.Debug("blsSignatureVerify input size wrong", "inputLen", inputLen)
1632-
return nil, ErrExecutionReverted
1648+
return [32]byte{}, nil, nil, ErrExecutionReverted
16331649
}
16341650

16351651
var msg [32]byte
@@ -1640,38 +1656,76 @@ func (c *blsSignatureVerify) Run(input []byte) ([]byte, error) {
16401656
sig, err := bls.SignatureFromBytes(signatureBytes)
16411657
if err != nil {
16421658
log.Debug("blsSignatureVerify invalid signature", "err", err)
1643-
return nil, ErrExecutionReverted
1659+
return [32]byte{}, nil, nil, ErrExecutionReverted
16441660
}
16451661

16461662
pubKeyNumber := (inputLen - msgAndSigLength) / singleBlsPubkeyLength
16471663
pubKeys := make([]bls.PublicKey, pubKeyNumber)
1664+
var seenPubKeys map[string]struct{}
1665+
if requireUniquePubKeys {
1666+
seenPubKeys = make(map[string]struct{}, pubKeyNumber)
1667+
}
16481668
for i := uint64(0); i < pubKeyNumber; i++ {
16491669
pubKeyBytes := getData(input, msgAndSigLength+i*singleBlsPubkeyLength, singleBlsPubkeyLength)
16501670
pubKey, err := bls.PublicKeyFromBytes(pubKeyBytes)
16511671
if err != nil {
16521672
log.Debug("blsSignatureVerify invalid pubKey", "err", err)
1653-
return nil, ErrExecutionReverted
1673+
return [32]byte{}, nil, nil, ErrExecutionReverted
1674+
}
1675+
if requireUniquePubKeys {
1676+
key := string(pubKeyBytes)
1677+
if _, ok := seenPubKeys[key]; ok {
1678+
return msg, sig, nil, nil
1679+
}
1680+
seenPubKeys[key] = struct{}{}
16541681
}
16551682
pubKeys[i] = pubKey
16561683
}
16571684

1658-
if pubKeyNumber > 1 {
1685+
return msg, sig, pubKeys, nil
1686+
}
1687+
1688+
func runBlsSignatureVerify(input []byte, requireUniquePubKeys bool) ([]byte, error) {
1689+
msg, sig, pubKeys, err := parseBlsSignatureVerifyInput(input, requireUniquePubKeys)
1690+
if err != nil {
1691+
return nil, err
1692+
}
1693+
if pubKeys == nil {
1694+
return common.Big0.Bytes(), nil
1695+
}
1696+
1697+
if len(pubKeys) > 1 {
16591698
if !sig.FastAggregateVerify(pubKeys, msg) {
16601699
return common.Big0.Bytes(), nil
16611700
}
16621701
} else {
1663-
if !sig.Verify(pubKeys[0], msgBytes) {
1702+
if !sig.Verify(pubKeys[0], msg[:]) {
16641703
return common.Big0.Bytes(), nil
16651704
}
16661705
}
16671706

16681707
return common.Big1.Bytes(), nil
16691708
}
16701709

1710+
// Run input:
1711+
// msg | signature | [{bls pubkey}] |
1712+
// 32 bytes | 96 bytes | [{48 bytes}] |
1713+
func (c *blsSignatureVerify) Run(input []byte) ([]byte, error) {
1714+
return runBlsSignatureVerify(input, false)
1715+
}
1716+
16711717
func (c *blsSignatureVerify) Name() string {
16721718
return "BLS_SIGNATURE_VERIFY"
16731719
}
16741720

1721+
func (c *blsSignatureVerifyMendel) Run(input []byte) ([]byte, error) {
1722+
return runBlsSignatureVerify(input, true)
1723+
}
1724+
1725+
func (c *blsSignatureVerifyMendel) Name() string {
1726+
return "BLS_SIGNATURE_VERIFY_MENDEL"
1727+
}
1728+
16751729
// kzgPointEvaluation implements the EIP-4844 point evaluation precompile.
16761730
type kzgPointEvaluation struct{}
16771731

core/vm/contracts_lightclient.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/url"
88
"strings"
99

10+
"github.com/cometbft/cometbft/types"
1011
"github.com/cosmos/iavl"
1112
"github.com/tendermint/tendermint/crypto/merkle"
1213
"github.com/tendermint/tendermint/crypto/secp256k1"
@@ -391,14 +392,22 @@ func (c *cometBFTLightBlockValidate) RequiredGas(input []byte) uint64 {
391392
return params.CometBFTLightBlockValidateGas
392393
}
393394

394-
func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool) (result []byte, err error) {
395+
func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool, requireUniqueValidators bool) (result []byte, err error) {
395396
defer func() {
396397
if r := recover(); r != nil {
397398
err = fmt.Errorf("internal error: %v\n", r)
398399
}
399400
}()
400401

401-
cs, block, err := v2.DecodeLightBlockValidationInput(input)
402+
var (
403+
cs *v2.ConsensusState
404+
block *types.LightBlock
405+
)
406+
if requireUniqueValidators {
407+
cs, block, err = v2.DecodeLightBlockValidationInputWithValidation(input)
408+
} else {
409+
cs, block, err = v2.DecodeLightBlockValidationInput(input)
410+
}
402411
if err != nil {
403412
return nil, err
404413
}
@@ -418,7 +427,7 @@ func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool) (result []b
418427
}
419428

420429
func (c *cometBFTLightBlockValidate) Run(input []byte) (result []byte, err error) {
421-
return c.run(input, false)
430+
return c.run(input, false, false)
422431
}
423432

424433
func (c *cometBFTLightBlockValidate) Name() string {
@@ -430,13 +439,25 @@ type cometBFTLightBlockValidateHertz struct {
430439
}
431440

432441
func (c *cometBFTLightBlockValidateHertz) Run(input []byte) (result []byte, err error) {
433-
return c.run(input, true)
442+
return c.run(input, true, false)
434443
}
435444

436445
func (c *cometBFTLightBlockValidateHertz) Name() string {
437446
return "COMET_BFT_LIGHT_BLOCK_VALIDATE_HERTZ"
438447
}
439448

449+
type cometBFTLightBlockValidateHertzMendel struct {
450+
cometBFTLightBlockValidate
451+
}
452+
453+
func (c *cometBFTLightBlockValidateHertzMendel) Run(input []byte) (result []byte, err error) {
454+
return c.run(input, true, true)
455+
}
456+
457+
func (c *cometBFTLightBlockValidateHertzMendel) Name() string {
458+
return "COMET_BFT_LIGHT_BLOCK_VALIDATE_HERTZ_MENDEL"
459+
}
460+
440461
// secp256k1SignatureRecover implemented as a native contract.
441462
type secp256k1SignatureRecover struct{}
442463

core/vm/contracts_lightclient_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import (
55
"encoding/hex"
66
"testing"
77

8+
"github.com/cometbft/cometbft/types"
89
"github.com/cosmos/iavl"
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/params"
912
"github.com/stretchr/testify/assert"
1013
"github.com/stretchr/testify/require"
1114
"github.com/tendermint/tendermint/crypto/merkle"
1215
cmn "github.com/tendermint/tendermint/libs/common"
1316

1417
v1 "github.com/ethereum/go-ethereum/core/vm/lightclient/v1"
18+
v2 "github.com/ethereum/go-ethereum/core/vm/lightclient/v2"
1519
)
1620

1721
const (
@@ -374,6 +378,39 @@ func TestCometBFTLightBlockValidateHertz(t *testing.T) {
374378
require.Equal(t, expectOutputStr, hex.EncodeToString(res))
375379
}
376380

381+
func TestCometBFTLightBlockValidateRejectsDuplicateTrustedValidatorsAtMendel(t *testing.T) {
382+
inputStr := "000000000000000000000000000000000000000000000000000000000000018c677265656e6669656c645f393030302d3132310000000000000000000000000000000000000000013c350cd55b99dc6c2b7da9bef5410fbfb869fede858e7b95bf7ca294e228bb40e33f6e876d63791ebd05ff617a1b4f4ad1aa2ce65e3c3a9cdfb33e0ffa7e8423000000000098968015154514f68ce65a0d9eecc578c0ab12da0a2a28a0805521b5b7ae56eb3fb24555efbfe59e1622bfe9f7be8c9022e9b3f2442739c1ce870b9adee169afe60f674edd7c86451c5363d89052fde8351895eeea166ce5373c36e31b518ed191d0c599aa0f5b0000000000989680432f6c4908a9aa5f3444421f466b11645235c99b831b2a2de9e504d7ea299e52a202ce529808618eb3bfc0addf13d8c5f2df821d81e18f9bc61583510b322d067d46323b0a572635c06a049c0a2a929e3c8184a50cf6a8b95708c25834ade456f399015a0000000000989680864cb9828254d712f8e59b164fc6a9402dc4e6c59065e38cff24f5323c8c5da888a0f97e5ee4ba1e11b0674b0a0d06204c1dfa247c370cd4be3e799fc4f6f48d977ac7ca0aeb060adb030a02080b1213677265656e6669656c645f393030302d3132311802220c08b2d7f3a10610e8d2adb3032a480a20ec6ecb5db4ffb17fabe40c60ca7b8441e9c5d77585d0831186f3c37aa16e9c15122408011220a2ab9e1eb9ea52812f413526e424b326aff2f258a56e00d690db9f805b60fe7e32200f40aeff672e8309b7b0aefbb9a1ae3d4299b5c445b7d54e8ff398488467f0053a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85542203c350cd55b99dc6c2b7da9bef5410fbfb869fede858e7b95bf7ca294e228bb404a203c350cd55b99dc6c2b7da9bef5410fbfb869fede858e7b95bf7ca294e228bb405220294d8fbd0b94b767a7eba9840f299a3586da7fe6b5dead3b7eecba193c400f935a20bc50557c12d7392b0d07d75df0b61232d48f86a74fdea6d1485d9be6317d268c6220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8556a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85572146699336aa109d1beab3946198c8e59f3b2cbd92f7a4065e3cd89e315ca39d87dee92835b98f8b8ec0861d6d9bb2c60156df5d375b3ceb1fbe71af6a244907d62548a694165caa660fec7a9b4e7b9198191361c71be0b128a0308021a480a20726abd0fdbfb6f779b0483e6e4b4b6f12241f6ea2bf374233ab1a316692b6415122408011220159f10ff15a8b58fc67a92ffd7f33c8cd407d4ce81b04ca79177dfd00ca19a67226808021214050cff76cc632760ba9db796c046004c900967361a0c08b3d7f3a10610808cadba03224080713027ffb776a702d78fd0406205c629ba473e1f8d6af646190f6eb9262cd67d69be90d10e597b91e06d7298eb6fa4b8f1eb7752ebf352a1f51560294548042268080212146699336aa109d1beab3946198c8e59f3b2cbd92f1a0c08b3d7f3a10610b087c1c00322405e2ddb70acfe4904438be3d9f4206c0ace905ac4fc306a42cfc9e86268950a0fbfd6ec5f526d3e41a3ef52bf9f9f358e3cb4c3feac76c762fa3651c1244fe004226808021214c55765fd2d0570e869f6ac22e7f2916a35ea300d1a0c08b3d7f3a10610f0b3d492032240ca17898bd22232fc9374e1188636ee321a396444a5b1a79f7628e4a11f265734b2ab50caf21e8092c55d701248e82b2f011426cb35ba22043b497a6b4661930612a0050aa8010a14050cff76cc632760ba9db796c046004c9009673612220a20e33f6e876d63791ebd05ff617a1b4f4ad1aa2ce65e3c3a9cdfb33e0ffa7e84231880ade2042080a6bbf6ffffffffff012a30a0805521b5b7ae56eb3fb24555efbfe59e1622bfe9f7be8c9022e9b3f2442739c1ce870b9adee169afe60f674edd7c86321415154514f68ce65a0d9eecc578c0ab12da0a2a283a14ee7a2a6a44d427f6949eeb8f12ea9fbb2501da880aa2010a146699336aa109d1beab3946198c8e59f3b2cbd92f12220a20451c5363d89052fde8351895eeea166ce5373c36e31b518ed191d0c599aa0f5b1880ade2042080ade2042a30831b2a2de9e504d7ea299e52a202ce529808618eb3bfc0addf13d8c5f2df821d81e18f9bc61583510b322d067d46323b3214432f6c4908a9aa5f3444421f466b11645235c99b3a14a0a7769429468054e19059af4867da0a495567e50aa2010a14c55765fd2d0570e869f6ac22e7f2916a35ea300d12220a200a572635c06a049c0a2a929e3c8184a50cf6a8b95708c25834ade456f399015a1880ade2042080ade2042a309065e38cff24f5323c8c5da888a0f97e5ee4ba1e11b0674b0a0d06204c1dfa247c370cd4be3e799fc4f6f48d977ac7ca3214864cb9828254d712f8e59b164fc6a9402dc4e6c53a143139916d97df0c589312b89950b6ab9795f34d1a12a8010a14050cff76cc632760ba9db796c046004c9009673612220a20e33f6e876d63791ebd05ff617a1b4f4ad1aa2ce65e3c3a9cdfb33e0ffa7e84231880ade2042080a6bbf6ffffffffff012a30a0805521b5b7ae56eb3fb24555efbfe59e1622bfe9f7be8c9022e9b3f2442739c1ce870b9adee169afe60f674edd7c86321415154514f68ce65a0d9eecc578c0ab12da0a2a283a14ee7a2a6a44d427f6949eeb8f12ea9fbb2501da88"
383+
384+
input, err := hex.DecodeString(inputStr)
385+
require.NoError(t, err)
386+
387+
cs, block, err := v2.DecodeLightBlockValidationInput(input)
388+
require.NoError(t, err)
389+
390+
duplicate := cs.ValidatorSet.Validators[0].Copy()
391+
cs.ValidatorSet = &types.ValidatorSet{Validators: []*types.Validator{duplicate.Copy(), duplicate.Copy(), duplicate.Copy()}}
392+
csBytes, err := cs.EncodeConsensusState()
393+
require.NoError(t, err)
394+
395+
blockProto, err := block.ToProto()
396+
require.NoError(t, err)
397+
blockBytes, err := blockProto.Marshal()
398+
require.NoError(t, err)
399+
400+
mutatedInput := make([]byte, 32)
401+
binary.BigEndian.PutUint64(mutatedInput[24:], uint64(len(csBytes)))
402+
mutatedInput = append(mutatedInput, csBytes...)
403+
mutatedInput = append(mutatedInput, blockBytes...)
404+
405+
mendelPrecompile := ActivePrecompiledContracts(params.Rules{IsOsaka: true, IsMendel: true})[common.BytesToAddress([]byte{0x67})]
406+
_, err = mendelPrecompile.Run(mutatedInput)
407+
require.ErrorContains(t, err, "duplicate validator")
408+
409+
legacyPrecompile := ActivePrecompiledContracts(params.Rules{IsOsaka: true})[common.BytesToAddress([]byte{0x67})]
410+
_, err = legacyPrecompile.Run(mutatedInput)
411+
require.NoError(t, err)
412+
}
413+
377414
func TestSecp256k1SignatureRecover(t *testing.T) {
378415
// local key
379416
{

core/vm/contracts_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525
"time"
2626

2727
"github.com/ethereum/go-ethereum/common"
28+
"github.com/ethereum/go-ethereum/params"
29+
"github.com/prysmaticlabs/prysm/v5/crypto/bls"
30+
blscommon "github.com/prysmaticlabs/prysm/v5/crypto/bls/common"
2831
)
2932

3033
// precompiledTest defines the input/output pairs for precompiled contract tests.
@@ -353,6 +356,84 @@ func TestPrecompiledBLS12381MapG2(t *testing.T) { testJson("blsMapG2", "f10
353356

354357
func TestPrecompiledBlsSignatureVerify(t *testing.T) { testJson("blsSignatureVerify", "66", t) }
355358

359+
func TestActivePrecompiledContractsUsesMendelVariants(t *testing.T) {
360+
rules := params.Rules{IsOsaka: true, IsMendel: true}
361+
precompiles := ActivePrecompiledContracts(rules)
362+
363+
requirePrecompile := func(addr byte) PrecompiledContract {
364+
t.Helper()
365+
366+
precompile, ok := precompiles[common.BytesToAddress([]byte{addr})]
367+
if !ok {
368+
t.Fatalf("missing precompile 0x%02x", addr)
369+
}
370+
return precompile
371+
}
372+
373+
if got := requirePrecompile(0x66).Name(); got != "BLS_SIGNATURE_VERIFY_MENDEL" {
374+
t.Fatalf("unexpected Mendel 0x66 precompile: %s", got)
375+
}
376+
if got := requirePrecompile(0x67).Name(); got != "COMET_BFT_LIGHT_BLOCK_VALIDATE_HERTZ_MENDEL" {
377+
t.Fatalf("unexpected Mendel 0x67 precompile: %s", got)
378+
}
379+
}
380+
381+
func TestBlsSignatureVerifyRejectsDuplicatePubKeysAtMendel(t *testing.T) {
382+
msg := [32]byte{'d', 'u', 'p'}
383+
384+
sk1, err := bls.RandKey()
385+
if err != nil {
386+
t.Fatal(err)
387+
}
388+
sk2, err := bls.RandKey()
389+
if err != nil {
390+
t.Fatal(err)
391+
}
392+
393+
pk1 := sk1.PublicKey()
394+
pk2 := sk2.PublicKey()
395+
sig1 := sk1.Sign(msg[:])
396+
dupAgg := bls.AggregateSignatures([]blscommon.Signature{sig1, sig1, sig1})
397+
398+
mendelRules := params.Rules{IsOsaka: true, IsMendel: true}
399+
mendelVerify := ActivePrecompiledContracts(mendelRules)[common.BytesToAddress([]byte{0x66})]
400+
401+
input := append(msg[:], dupAgg.Marshal()...)
402+
input = append(input, pk1.Marshal()...)
403+
input = append(input, pk1.Marshal()...)
404+
input = append(input, pk1.Marshal()...)
405+
406+
res, err := mendelVerify.Run(input)
407+
if err != nil {
408+
t.Fatalf("unexpected error: %v", err)
409+
}
410+
if len(res) != 0 {
411+
t.Fatalf("expected Mendel duplicate-pubkey verification to fail, got %x", res)
412+
}
413+
414+
osakaRules := params.Rules{IsOsaka: true}
415+
osakaVerify := ActivePrecompiledContracts(osakaRules)[common.BytesToAddress([]byte{0x66})]
416+
legacyRes, err := osakaVerify.Run(input)
417+
if err != nil {
418+
t.Fatalf("unexpected Osaka error: %v", err)
419+
}
420+
if len(legacyRes) == 0 {
421+
t.Fatalf("expected pre-Mendel duplicate-pubkey verification to succeed")
422+
}
423+
424+
mixedInput := append(msg[:], dupAgg.Marshal()...)
425+
mixedInput = append(mixedInput, pk1.Marshal()...)
426+
mixedInput = append(mixedInput, pk2.Marshal()...)
427+
mixedInput = append(mixedInput, pk2.Marshal()...)
428+
mixedRes, err := mendelVerify.Run(mixedInput)
429+
if err != nil {
430+
t.Fatalf("unexpected mixed-key error: %v", err)
431+
}
432+
if len(mixedRes) != 0 {
433+
t.Fatalf("expected mixed duplicate-signature verification to fail, got %x", mixedRes)
434+
}
435+
}
436+
356437
func TestPrecompiledPointEvaluation(t *testing.T) { testJson("pointEvaluation", "0a", t) }
357438

358439
func BenchmarkPrecompiledPointEvaluation(b *testing.B) { benchJson("pointEvaluation", "0a", b) }

0 commit comments

Comments
 (0)