github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/accounts_list_test.go (about) 1 package accounts 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "math" 8 "os" 9 "strconv" 10 "strings" 11 "testing" 12 13 "github.com/golang/mock/gomock" 14 "github.com/google/uuid" 15 types "github.com/prysmaticlabs/eth2-types" 16 ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" 17 validatorpb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2" 18 "github.com/prysmaticlabs/prysm/shared/bls" 19 "github.com/prysmaticlabs/prysm/shared/bytesutil" 20 "github.com/prysmaticlabs/prysm/shared/event" 21 "github.com/prysmaticlabs/prysm/shared/mock" 22 "github.com/prysmaticlabs/prysm/shared/petnames" 23 "github.com/prysmaticlabs/prysm/shared/testutil/assert" 24 "github.com/prysmaticlabs/prysm/shared/testutil/require" 25 "github.com/prysmaticlabs/prysm/validator/accounts/wallet" 26 "github.com/prysmaticlabs/prysm/validator/keymanager" 27 "github.com/prysmaticlabs/prysm/validator/keymanager/derived" 28 "github.com/prysmaticlabs/prysm/validator/keymanager/imported" 29 "github.com/prysmaticlabs/prysm/validator/keymanager/remote" 30 constant "github.com/prysmaticlabs/prysm/validator/testing" 31 keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" 32 ) 33 34 type mockRemoteKeymanager struct { 35 publicKeys [][48]byte 36 opts *remote.KeymanagerOpts 37 } 38 39 func (m *mockRemoteKeymanager) FetchValidatingPublicKeys(_ context.Context) ([][48]byte, error) { 40 return m.publicKeys, nil 41 } 42 43 func (m *mockRemoteKeymanager) Sign(context.Context, *validatorpb.SignRequest) (bls.Signature, error) { 44 return nil, nil 45 } 46 47 func (m *mockRemoteKeymanager) SubscribeAccountChanges(_ chan [][48]byte) event.Subscription { 48 return nil 49 } 50 51 func createRandomKeystore(t testing.TB, password string) *keymanager.Keystore { 52 encryptor := keystorev4.New() 53 id, err := uuid.NewRandom() 54 require.NoError(t, err) 55 validatingKey, err := bls.RandKey() 56 require.NoError(t, err) 57 pubKey := validatingKey.PublicKey().Marshal() 58 cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password) 59 require.NoError(t, err) 60 return &keymanager.Keystore{ 61 Crypto: cryptoFields, 62 Pubkey: fmt.Sprintf("%x", pubKey), 63 ID: id.String(), 64 Version: encryptor.Version(), 65 Name: encryptor.Name(), 66 } 67 } 68 69 func TestListAccounts_ImportedKeymanager(t *testing.T) { 70 walletDir, passwordsDir, walletPasswordFile := setupWalletAndPasswordsDir(t) 71 cliCtx := setupWalletCtx(t, &testWalletConfig{ 72 walletDir: walletDir, 73 passwordsDir: passwordsDir, 74 keymanagerKind: keymanager.Imported, 75 walletPasswordFile: walletPasswordFile, 76 }) 77 w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ 78 WalletCfg: &wallet.Config{ 79 WalletDir: walletDir, 80 KeymanagerKind: keymanager.Imported, 81 WalletPassword: "Passwordz0320$", 82 }, 83 }) 84 require.NoError(t, err) 85 km, err := imported.NewKeymanager( 86 cliCtx.Context, 87 &imported.SetupConfig{ 88 Wallet: w, 89 ListenForChanges: false, 90 }, 91 ) 92 require.NoError(t, err) 93 94 numAccounts := 5 95 keystores := make([]*keymanager.Keystore, numAccounts) 96 for i := 0; i < numAccounts; i++ { 97 keystores[i] = createRandomKeystore(t, password) 98 } 99 require.NoError(t, km.ImportKeystores(cliCtx.Context, keystores, password)) 100 101 rescueStdout := os.Stdout 102 r, writer, err := os.Pipe() 103 require.NoError(t, err) 104 os.Stdout = writer 105 106 // We call the list imported keymanager accounts function. 107 require.NoError( 108 t, 109 listImportedKeymanagerAccounts( 110 context.Background(), 111 true, /* show deposit data */ 112 true, /*show private keys */ 113 km, 114 ), 115 ) 116 117 require.NoError(t, writer.Close()) 118 out, err := ioutil.ReadAll(r) 119 require.NoError(t, err) 120 os.Stdout = rescueStdout 121 122 // Get stdout content and split to lines 123 newLine := fmt.Sprintln() 124 lines := strings.Split(string(out), newLine) 125 126 // Expected output example: 127 /* 128 (keymanager kind) imported wallet 129 130 Showing 5 validator accounts 131 View the eth1 deposit transaction data for your accounts by running `validator accounts list --show-deposit-data 132 133 Account 0 | fully-evolving-fawn 134 [validating public key] 0xa6669aa0381c06470b9a6faf8abf4194ad5148a62e461cbef5a6bc4d292026f58b992c4cf40e50552d301cef19da75b9 135 [validating private key] 0x50cabc13435fcbde9d240fe720aff84f8557a6c1c445211b904f1a9620668241 136 If you imported your account coming from the Ethereum launchpad, you will find your deposit_data.json in the eth2.0-deposit-cli's validator_keys folder 137 138 139 Account 1 | preferably-mighty-heron 140 [validating public key] 0xa7ea37fa2e2272762ffed8486f09b13cd56d76cf03a2a3e75bc36bd1719add84c20597671750be5bc1ccd3dadfebc30f 141 [validating private key] 0x44563da0d11bc6a7219d18217cce8cdd064de3ebee5cdcf8d901c2fae7545116 142 If you imported your account coming from the Ethereum eth2 launchpad, you will find your deposit_data.json in the eth2.0-deposit-cli's validator_keys folder 143 144 145 Account 2 | conversely-good-monitor 146 [validating public key] 0xa4c63619fb8cb87f6dd1686c9255f99c68066797bf284488ecbab64b1926d33eefdf96d1ee89ae4a89e84e7fb019d5e5 147 [validating private key] 0x4448d0ab17ecd73bbb636ddbfc89b181731f6cd88c33f2cecc0d04cba1a18447 148 If you imported your account coming from the Ethereum eth2 launchpad, you will find your deposit_data.json in the eth2.0-deposit-cli's validator_keys folder 149 150 151 Account 3 | rarely-joint-mako 152 [validating public key] 0x91dd8d5bfc22aea398740ebcea66ced159df8d3f1a066d7aba9f0bef4ed6d9687fc1fd1c87bd2b6d12b0788dfb6a7d20 153 [validating private key] 0x4d1944bd7375185f70b3e70c68d9e6307f2009de3a4cf47ca5217443ddf81fc9 154 If you imported your account coming from the Ethereum eth2 launchpad, you will find your deposit_data.json in the eth2.0-deposit-cli's validator_keys folder 155 156 157 Account 4 | mainly-useful-catfish 158 [validating public key] 0x83c4d722a98b599e2666bbe35146ff44800256190bc662f2dd5efbc0c4c0d57e5d297487a4f9c21a932d3b1b40e8379f 159 [validating private key] 0x284cd65030496bf82ee2d52963cd540a1abb2cc738b8164901bbe7e2df4d57bd 160 If you imported your account coming from the Ethereum eth2 launchpad, you will find your deposit_data.json in the eth2.0-deposit-cli's validator_keys folder 161 162 163 164 */ 165 166 // Expected output format definition 167 const prologLength = 4 168 const accountLength = 6 169 const epilogLength = 2 170 const nameOffset = 1 171 const keyOffset = 2 172 const privkeyOffset = 3 173 174 // Require the output has correct number of lines 175 lineCount := prologLength + accountLength*numAccounts + epilogLength 176 require.Equal(t, lineCount, len(lines)) 177 178 // Assert the keymanager kind is printed on the first line. 179 kindString := "imported" 180 kindFound := strings.Contains(lines[0], kindString) 181 assert.Equal(t, true, kindFound, "Keymanager Kind %s not found on the first line", kindString) 182 183 // Get account names and require the correct count 184 accountNames, err := km.ValidatingAccountNames() 185 require.NoError(t, err) 186 require.Equal(t, numAccounts, len(accountNames)) 187 188 // Assert that account names are printed on the correct lines 189 for i, accountName := range accountNames { 190 lineNumber := prologLength + accountLength*i + nameOffset 191 accountNameFound := strings.Contains(lines[lineNumber], accountName) 192 assert.Equal(t, true, accountNameFound, "Account Name %s not found on line number %d", accountName, lineNumber) 193 } 194 195 // Get public keys and require the correct count 196 pubKeys, err := km.FetchValidatingPublicKeys(cliCtx.Context) 197 require.NoError(t, err) 198 require.Equal(t, numAccounts, len(pubKeys)) 199 200 // Assert that public keys are printed on the correct lines 201 for i, key := range pubKeys { 202 lineNumber := prologLength + accountLength*i + keyOffset 203 keyString := fmt.Sprintf("%#x", key) 204 keyFound := strings.Contains(lines[lineNumber], keyString) 205 assert.Equal(t, true, keyFound, "Public Key %s not found on line number %d", keyString, lineNumber) 206 } 207 208 // Get private keys and require the correct count 209 privKeys, err := km.FetchValidatingPrivateKeys(cliCtx.Context) 210 require.NoError(t, err) 211 require.Equal(t, numAccounts, len(pubKeys)) 212 213 // Assert that private keys are printed on the correct lines 214 for i, key := range privKeys { 215 lineNumber := prologLength + accountLength*i + privkeyOffset 216 keyString := fmt.Sprintf("%#x", key) 217 keyFound := strings.Contains(lines[lineNumber], keyString) 218 assert.Equal(t, true, keyFound, "Private Key %s not found on line number %d", keyString, lineNumber) 219 } 220 } 221 222 func TestListAccounts_DerivedKeymanager(t *testing.T) { 223 walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t) 224 cliCtx := setupWalletCtx(t, &testWalletConfig{ 225 walletDir: walletDir, 226 passwordsDir: passwordsDir, 227 keymanagerKind: keymanager.Derived, 228 walletPasswordFile: passwordFilePath, 229 }) 230 w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ 231 WalletCfg: &wallet.Config{ 232 WalletDir: walletDir, 233 KeymanagerKind: keymanager.Derived, 234 WalletPassword: "Passwordz0320$", 235 }, 236 }) 237 require.NoError(t, err) 238 239 keymanager, err := derived.NewKeymanager( 240 cliCtx.Context, 241 &derived.SetupConfig{ 242 Wallet: w, 243 ListenForChanges: false, 244 }, 245 ) 246 require.NoError(t, err) 247 248 numAccounts := 5 249 err = keymanager.RecoverAccountsFromMnemonic(cliCtx.Context, constant.TestMnemonic, "", numAccounts) 250 require.NoError(t, err) 251 252 rescueStdout := os.Stdout 253 r, writer, err := os.Pipe() 254 require.NoError(t, err) 255 os.Stdout = writer 256 257 // We call the list imported keymanager accounts function. 258 require.NoError(t, listDerivedKeymanagerAccounts(cliCtx.Context, true, keymanager)) 259 260 require.NoError(t, writer.Close()) 261 out, err := ioutil.ReadAll(r) 262 require.NoError(t, err) 263 os.Stdout = rescueStdout 264 265 // Get stdout content and split to lines 266 newLine := fmt.Sprintln() 267 lines := strings.Split(string(out), newLine) 268 269 // Expected output example: 270 /* 271 (keymanager kind) derived, (HD) hierarchical-deterministic 272 (derivation format) m / purpose / coin_type / account_index / withdrawal_key / validating_key 273 Showing 2 validator accounts 274 275 Account 0 | uniquely-sunny-tarpon 276 [withdrawal public key] 0xa5faa97252104b408340b5d8cae3fa01023fa4dc9e7c7b470821433cf3a2a18158410b7d8a6dcdcd176c6552c2526681 277 [withdrawal private key] 0x5266fd1f13d7af74614fde4fed3b664bfd529bc4ad91118e3db73647b99546df 278 [derivation path] m/12381/3600/0/0 279 [validating public key] 0xa7292d8f8d1c1f3d42cacefd2fc4cd3b82651be37c1eb790bbd294a874829f4b7e1c167345dcc1966cc844132b38097e 280 [validating private key] 0x590707187dae64b42b8d36a95f3d7e11313ddd8b8d871b09e478e08c9bc8740b 281 [derivation path] m/12381/3600/0/0/0 282 283 ======================Eth1 Deposit Transaction Data===================== 284 285 0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001205a9e92992d6a97ad113d217fa35cbe0659c662afe913ffd3a3ba61d7473be5630000000000000000000000000000000000000000000000000000000000000030a7292d8f8d1c1f3d42cacefd2fc4cd3b82651be37c1eb790bbd294a874829f4b7e1c167345dcc1966cc844132b38097e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020003b8f70706c37fb0b8dcbd95340889bad7d7f29121ea895052a8b216de95e480000000000000000000000000000000000000000000000000000000000000060b6727242b055448defbf54292c65e30ae28ca3aef8a07c8fe674abc0ca42a324be2e7592d3e45bba84ca364d7fe1f0ce073bf8b3692246395aa127cdbf93c64ae9ca48f85cb4b1e519f6821998181de1c7465b2bdcae4ddd0dbc2d02a56219d9 286 287 =================================================================== 288 289 Account 1 | usually-obliging-pelican 290 [withdrawal public key] 0xb91840d33bb87338bb28605cff837acd50e43a174a8a6d3893108fb91217fa428c12f1b2a25cf3c7aca75d418bcf0384 291 [withdrawal private key] 0x72c5ffa7d08fb16cd35a9cb10494dfd49b46842ea1bcc1a4cf46b46680b66810 292 [derivation path] m/12381/3600/1/0 293 [validating public key] 0x8447f878b701dad4dfa5a884cebc4745b0e8f21340dc56c840826537764dcc54e2e68f80b8d4e5737180212a26211891 294 [validating private key] 0x2cd5b1cddc9d96e50a16bea05d0953447655e3dd59fa1bfefad467c73d6c164a 295 [derivation path] m/12381/3600/1/0/0 296 297 ======================Eth1 Deposit Transaction Data===================== 298 299 0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001200a0b9079c33cc40d602a50f5c51f6db30b0f959fc6f58048d6d43319fea6c09000000000000000000000000000000000000000000000000000000000000000308447f878b701dad4dfa5a884cebc4745b0e8f21340dc56c840826537764dcc54e2e68f80b8d4e5737180212a2621189100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000d6ac42bde23388e7428c1247364347c027c3507e461d68b851d506c60364cf0000000000000000000000000000000000000000000000000000000000000060801a2d432595164d7d88ae1695618db511d1507108573b8471098536b2b5a23f6711235f0a9c6fa65ac26cbd0f2d97e013e0c72ab6b5cff406c48d99ec0a2439aa9faa4557d20bb210d451519101616fa20b1ff2c67fae561cdff160fbc7dc98 300 301 =================================================================== 302 303 304 */ 305 306 // Expected output format definition 307 const prologLength = 3 308 const accountLength = 6 309 const epilogLength = 1 310 const nameOffset = 1 311 const keyOffset = 2 312 const validatingPrivateKeyOffset = 3 313 314 // Require the output has correct number of lines 315 lineCount := prologLength + accountLength*numAccounts + epilogLength 316 require.Equal(t, lineCount, len(lines)) 317 318 // Assert the keymanager kind is printed on the first line. 319 kindString := w.KeymanagerKind().String() 320 kindFound := strings.Contains(lines[0], kindString) 321 assert.Equal(t, true, kindFound, "Keymanager Kind %s not found on the first line", kindString) 322 323 // Get account names and require the correct count 324 accountNames, err := keymanager.ValidatingAccountNames(cliCtx.Context) 325 require.NoError(t, err) 326 require.Equal(t, numAccounts, len(accountNames)) 327 328 // Assert that account names are printed on the correct lines 329 for i, accountName := range accountNames { 330 lineNumber := prologLength + accountLength*i + nameOffset 331 accountNameFound := strings.Contains(lines[lineNumber], accountName) 332 assert.Equal(t, true, accountNameFound, "Account Name %s not found on line number %d", accountName, lineNumber) 333 } 334 335 // Get public keys and require the correct count 336 pubKeys, err := keymanager.FetchValidatingPublicKeys(cliCtx.Context) 337 require.NoError(t, err) 338 require.Equal(t, numAccounts, len(pubKeys)) 339 340 // Assert that public keys are printed on the correct lines 341 for i, key := range pubKeys { 342 lineNumber := prologLength + accountLength*i + keyOffset 343 keyString := fmt.Sprintf("%#x", key) 344 keyFound := strings.Contains(lines[lineNumber], keyString) 345 assert.Equal(t, true, keyFound, "Public Key %s not found on line number %d", keyString, lineNumber) 346 } 347 348 // Get validating private keys and require the correct count 349 validatingPrivKeys, err := keymanager.FetchValidatingPrivateKeys(cliCtx.Context) 350 require.NoError(t, err) 351 require.Equal(t, numAccounts, len(pubKeys)) 352 353 // Assert that validating private keys are printed on the correct lines 354 for i, key := range validatingPrivKeys { 355 lineNumber := prologLength + accountLength*i + validatingPrivateKeyOffset 356 keyString := fmt.Sprintf("%#x", key) 357 keyFound := strings.Contains(lines[lineNumber], keyString) 358 assert.Equal(t, true, keyFound, "Validating Private Key %s not found on line number %d", keyString, lineNumber) 359 } 360 } 361 362 func TestListAccounts_RemoteKeymanager(t *testing.T) { 363 walletDir, _, _ := setupWalletAndPasswordsDir(t) 364 cliCtx := setupWalletCtx(t, &testWalletConfig{ 365 walletDir: walletDir, 366 keymanagerKind: keymanager.Remote, 367 }) 368 w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{ 369 WalletCfg: &wallet.Config{ 370 WalletDir: walletDir, 371 KeymanagerKind: keymanager.Remote, 372 WalletPassword: password, 373 }, 374 }) 375 require.NoError(t, err) 376 377 rescueStdout := os.Stdout 378 r, writer, err := os.Pipe() 379 require.NoError(t, err) 380 os.Stdout = writer 381 382 numAccounts := 3 383 pubKeys := make([][48]byte, numAccounts) 384 for i := 0; i < numAccounts; i++ { 385 key := make([]byte, 48) 386 copy(key, strconv.Itoa(i)) 387 pubKeys[i] = bytesutil.ToBytes48(key) 388 } 389 km := &mockRemoteKeymanager{ 390 publicKeys: pubKeys, 391 opts: &remote.KeymanagerOpts{ 392 RemoteCertificate: &remote.CertificateConfig{ 393 RequireTls: true, 394 ClientCertPath: "/tmp/client.crt", 395 ClientKeyPath: "/tmp/client.key", 396 CACertPath: "/tmp/ca.crt", 397 }, 398 RemoteAddr: "localhost:4000", 399 }, 400 } 401 // We call the list remote keymanager accounts function. 402 require.NoError(t, listRemoteKeymanagerAccounts(context.Background(), w, km, km.opts)) 403 404 require.NoError(t, writer.Close()) 405 out, err := ioutil.ReadAll(r) 406 require.NoError(t, err) 407 os.Stdout = rescueStdout 408 409 // Get stdout content and split to lines 410 newLine := fmt.Sprintln() 411 lines := strings.Split(string(out), newLine) 412 413 // Expected output example: 414 /* 415 (keymanager kind) remote signer 416 (configuration file path) /tmp/79336/wallet/remote/keymanageropts.json 417 418 Configuration options 419 Remote gRPC address: localhost:4000 420 Require TLS: true 421 Client cert path: /tmp/client.crt 422 Client key path: /tmp/client.key 423 CA cert path: /tmp/ca.crt 424 425 Showing 3 validator accounts 426 427 equally-primary-foal 428 [validating public key] 0x300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 429 430 431 rationally-charmed-werewolf 432 [validating public key] 0x310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 433 434 435 */ 436 437 // Expected output format definition 438 const prologLength = 11 439 const configOffset = 4 440 const configLength = 5 441 const accountLength = 4 442 const nameOffset = 1 443 const keyOffset = 2 444 const epilogLength = 1 445 446 // Require the output has correct number of lines 447 lineCount := prologLength + accountLength*numAccounts + epilogLength 448 require.Equal(t, lineCount, len(lines)) 449 450 // Assert the keymanager kind is printed on the first line. 451 kindString := w.KeymanagerKind().String() 452 kindFound := strings.Contains(lines[0], kindString) 453 assert.Equal(t, true, kindFound, "Keymanager Kind %s not found on the first line", kindString) 454 455 // Assert that Configuration is printed in the right position 456 configLines := lines[configOffset:(configOffset + configLength)] 457 configExpected := km.opts.String() 458 configActual := fmt.Sprintln(strings.Join(configLines, newLine)) 459 assert.Equal(t, configExpected, configActual, "Configuration not found at the expected position") 460 461 // Assert that account names are printed on the correct lines 462 for i := 0; i < numAccounts; i++ { 463 lineNumber := prologLength + accountLength*i + nameOffset 464 accountName := petnames.DeterministicName(pubKeys[i][:], "-") 465 accountNameFound := strings.Contains(lines[lineNumber], accountName) 466 assert.Equal(t, true, accountNameFound, "Account Name %s not found on line number %d", accountName, lineNumber) 467 } 468 469 // Assert that public keys are printed on the correct lines 470 for i, key := range pubKeys { 471 lineNumber := prologLength + accountLength*i + keyOffset 472 keyString := fmt.Sprintf("%#x", key) 473 keyFound := strings.Contains(lines[lineNumber], keyString) 474 assert.Equal(t, true, keyFound, "Public Key %s not found on line number %d", keyString, lineNumber) 475 } 476 } 477 478 func TestListAccounts_ListValidatorIndices(t *testing.T) { 479 ctrl := gomock.NewController(t) 480 defer ctrl.Finish() 481 482 numAccounts := 3 483 pubKeys := make([][48]byte, numAccounts) 484 pks := make([][]byte, numAccounts) 485 486 for i := 0; i < numAccounts; i++ { 487 key := make([]byte, 48) 488 copy(key, strconv.Itoa(i)) 489 pubKeys[i] = bytesutil.ToBytes48(key) 490 pks[i] = key 491 } 492 493 km := &mockRemoteKeymanager{ 494 publicKeys: pubKeys, 495 } 496 497 rescueStdout := os.Stdout 498 r, writer, err := os.Pipe() 499 require.NoError(t, err) 500 os.Stdout = writer 501 502 m := mock.NewMockBeaconNodeValidatorClient(ctrl) 503 504 req := ðpb.MultipleValidatorStatusRequest{PublicKeys: pks} 505 resp := ðpb.MultipleValidatorStatusResponse{Indices: []types.ValidatorIndex{1, math.MaxUint64, 2}} 506 507 m. 508 EXPECT(). 509 MultipleValidatorStatus(gomock.Eq(context.Background()), gomock.Eq(req)). 510 Return(resp, nil) 511 512 require.NoError( 513 t, 514 listValidatorIndices( 515 context.Background(), 516 km, 517 m, 518 ), 519 ) 520 521 require.NoError(t, writer.Close()) 522 out, err := ioutil.ReadAll(r) 523 require.NoError(t, err) 524 os.Stdout = rescueStdout 525 526 expectedStdout := au.BrightGreen("Validator indices:").Bold().String() + "\n0x30000000: 1\n0x32000000: 2\n" 527 require.Equal(t, expectedStdout, string(out)) 528 }