github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/internal/pkg/gateway/api.go (about) 1 /* 2 Copyright 2021 IBM All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package gateway 8 9 import ( 10 "context" 11 "fmt" 12 "io" 13 "math/rand" 14 "strings" 15 16 "github.com/golang/protobuf/proto" 17 "github.com/hechain20/hechain/common/flogging" 18 "github.com/hechain20/hechain/common/ledger" 19 "github.com/hechain20/hechain/core/aclmgmt/resources" 20 "github.com/hechain20/hechain/core/chaincode" 21 "github.com/hechain20/hechain/internal/pkg/gateway/event" 22 "github.com/hechain20/hechain/protoutil" 23 "github.com/hyperledger/fabric-protos-go/common" 24 gp "github.com/hyperledger/fabric-protos-go/gateway" 25 ab "github.com/hyperledger/fabric-protos-go/orderer" 26 "github.com/hyperledger/fabric-protos-go/peer" 27 "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/status" 29 ) 30 31 type endorserResponse struct { 32 action *peer.ChaincodeEndorsedAction 33 err *gp.ErrorDetail 34 timeoutExpired bool 35 } 36 37 // Evaluate will invoke the transaction function as specified in the SignedProposal 38 func (gs *Server) Evaluate(ctx context.Context, request *gp.EvaluateRequest) (*gp.EvaluateResponse, error) { 39 if request == nil { 40 return nil, status.Error(codes.InvalidArgument, "an evaluate request is required") 41 } 42 signedProposal := request.GetProposedTransaction() 43 channel, chaincodeID, hasTransientData, err := getChannelAndChaincodeFromSignedProposal(signedProposal) 44 if err != nil { 45 return nil, status.Errorf(codes.InvalidArgument, "failed to unpack transaction proposal: %s", err) 46 } 47 48 err = gs.registry.connectChannelPeers(channel, false) 49 if err != nil { 50 return nil, status.Errorf(codes.Unavailable, "%s", err) 51 } 52 53 targetOrgs := request.GetTargetOrganizations() 54 transientProtected := false 55 if hasTransientData && targetOrgs == nil { 56 targetOrgs = []string{gs.registry.localEndorser.mspid} 57 transientProtected = true 58 } 59 60 plan, err := gs.registry.evaluator(channel, chaincodeID, targetOrgs) 61 if err != nil { 62 if transientProtected { 63 return nil, status.Errorf(codes.FailedPrecondition, "no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: %s", err) 64 } 65 return nil, status.Errorf(codes.FailedPrecondition, "%s", err) 66 } 67 68 endorser := plan.endorsers()[0] 69 var response *peer.Response 70 var errDetails []proto.Message 71 for response == nil { 72 gs.logger.Debugw("Sending to peer:", "channel", channel, "chaincode", chaincodeID, "txID", request.TransactionId, "MSPID", endorser.mspid, "endpoint", endorser.address) 73 74 done := make(chan error) 75 go func() { 76 defer close(done) 77 ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) 78 defer cancel() 79 pr, err := endorser.client.ProcessProposal(ctx, signedProposal) 80 code, message, retry, remove := responseStatus(pr, err) 81 if code == codes.OK { 82 response = pr.Response 83 // Prefer result from proposal response as Response.Payload is not required to be transaction result 84 if result, err := getResultFromProposalResponse(pr); err == nil { 85 response.Payload = result 86 } else { 87 logger.Warnw("Successful proposal response contained no transaction result", "error", err.Error(), "chaincode", chaincodeID, "channel", channel, "txID", request.TransactionId, "endorserAddress", endorser.endpointConfig.address, "endorserMspid", endorser.endpointConfig.mspid, "status", response.Status, "message", response.Message) 88 } 89 } else { 90 logger.Debugw("Evaluate call to endorser failed", "chaincode", chaincodeID, "channel", channel, "txID", request.TransactionId, "endorserAddress", endorser.endpointConfig.address, "endorserMspid", endorser.endpointConfig.mspid, "error", message) 91 errDetails = append(errDetails, errorDetail(endorser.endpointConfig, message)) 92 if remove { 93 gs.registry.removeEndorser(endorser) 94 } 95 if retry { 96 endorser = plan.nextPeerInGroup(endorser) 97 } else { 98 done <- newRpcError(code, "evaluate call to endorser returned error: "+message, errDetails...) 99 } 100 if endorser == nil { 101 done <- newRpcError(code, "failed to evaluate transaction, see attached details for more info", errDetails...) 102 } 103 } 104 }() 105 select { 106 case status := <-done: 107 if status != nil { 108 return nil, status 109 } 110 case <-ctx.Done(): 111 // Overall evaluation timeout expired 112 logger.Warnw("Evaluate call timed out while processing request", "channel", request.ChannelId, "txID", request.TransactionId) 113 return nil, newRpcError(codes.DeadlineExceeded, "evaluate timeout expired") 114 } 115 } 116 117 evaluateResponse := &gp.EvaluateResponse{ 118 Result: response, 119 } 120 121 logger.Debugw("Evaluate call to endorser returned success", "channel", request.ChannelId, "txID", request.TransactionId, "endorserAddress", endorser.endpointConfig.address, "endorserMspid", endorser.endpointConfig.mspid, "status", response.GetStatus(), "message", response.GetMessage()) 122 return evaluateResponse, nil 123 } 124 125 // Endorse will collect endorsements by invoking the transaction function specified in the SignedProposal against 126 // sufficient Peers to satisfy the endorsement policy. 127 func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp.EndorseResponse, error) { 128 if request == nil { 129 return nil, status.Error(codes.InvalidArgument, "an endorse request is required") 130 } 131 signedProposal := request.GetProposedTransaction() 132 if signedProposal == nil { 133 return nil, status.Error(codes.InvalidArgument, "the proposed transaction must contain a signed proposal") 134 } 135 proposal, err := protoutil.UnmarshalProposal(signedProposal.ProposalBytes) 136 if err != nil { 137 return nil, status.Error(codes.InvalidArgument, err.Error()) 138 } 139 header, err := protoutil.UnmarshalHeader(proposal.Header) 140 if err != nil { 141 return nil, status.Error(codes.InvalidArgument, err.Error()) 142 } 143 channelHeader, err := protoutil.UnmarshalChannelHeader(header.ChannelHeader) 144 if err != nil { 145 return nil, status.Error(codes.InvalidArgument, err.Error()) 146 } 147 payload, err := protoutil.UnmarshalChaincodeProposalPayload(proposal.Payload) 148 if err != nil { 149 return nil, status.Error(codes.InvalidArgument, err.Error()) 150 } 151 spec, err := protoutil.UnmarshalChaincodeInvocationSpec(payload.Input) 152 if err != nil { 153 return nil, status.Error(codes.InvalidArgument, err.Error()) 154 } 155 156 channel := channelHeader.ChannelId 157 chaincodeID := spec.GetChaincodeSpec().GetChaincodeId().GetName() 158 hasTransientData := len(payload.GetTransientMap()) > 0 159 160 logger := gs.logger.With("channel", channel, "chaincode", chaincodeID, "txID", request.TransactionId) 161 162 var plan *plan 163 var action *peer.ChaincodeEndorsedAction 164 if len(request.EndorsingOrganizations) > 0 { 165 // The client is specifying the endorsing orgs and taking responsibility for ensuring it meets the signature policy 166 plan, err = gs.registry.planForOrgs(channel, chaincodeID, request.EndorsingOrganizations) 167 if err != nil { 168 return nil, status.Error(codes.Unavailable, err.Error()) 169 } 170 } else { 171 // The client is delegating choice of endorsers to the gateway. 172 plan, err = gs.planFromFirstEndorser(ctx, channel, chaincodeID, hasTransientData, signedProposal, logger) 173 if err != nil { 174 return nil, err 175 } 176 } 177 178 for plan.completedLayout == nil { 179 // loop through the layouts until one gets satisfied 180 endorsers := plan.endorsers() 181 if endorsers == nil { 182 // no more layouts 183 break 184 } 185 // send to all the endorsers 186 waitCh := make(chan bool, len(endorsers)) 187 for _, e := range endorsers { 188 go func(e *endorser) { 189 for e != nil { 190 if gs.processProposal(ctx, plan, e, signedProposal, logger) { 191 break 192 } 193 e = plan.nextPeerInGroup(e) 194 } 195 waitCh <- true 196 }(e) 197 } 198 for i := 0; i < len(endorsers); i++ { 199 select { 200 case <-waitCh: 201 // Endorser completedLayout normally 202 case <-ctx.Done(): 203 logger.Warnw("Endorse call timed out while collecting endorsements", "numEndorsers", len(endorsers)) 204 return nil, newRpcError(codes.DeadlineExceeded, "endorsement timeout expired while collecting endorsements") 205 } 206 } 207 208 } 209 210 if plan.completedLayout == nil { 211 return nil, newRpcError(codes.Aborted, "failed to collect enough transaction endorsements, see attached details for more info", plan.errorDetails...) 212 } 213 214 action = &peer.ChaincodeEndorsedAction{ProposalResponsePayload: plan.responsePayload, Endorsements: uniqueEndorsements(plan.completedLayout.endorsements)} 215 216 preparedTransaction, err := prepareTransaction(header, payload, action) 217 if err != nil { 218 return nil, status.Errorf(codes.Aborted, "failed to assemble transaction: %s", err) 219 } 220 221 return &gp.EndorseResponse{PreparedTransaction: preparedTransaction}, nil 222 } 223 224 type ppResponse struct { 225 response *peer.ProposalResponse 226 err error 227 } 228 229 // processProposal will invoke the given endorsing peer to process the signed proposal, and will update the plan accordingly. 230 // This function will timeout and return false if the given context timeout or the EndorsementTimeout option expires. 231 // Returns boolean true if the endorsement was successful. 232 func (gs *Server) processProposal(ctx context.Context, plan *plan, endorser *endorser, signedProposal *peer.SignedProposal, logger *flogging.FabricLogger) bool { 233 var response *peer.ProposalResponse 234 done := make(chan *ppResponse) 235 go func() { 236 defer close(done) 237 logger.Debugw("Sending to endorser:", "MSPID", endorser.mspid, "endpoint", endorser.address) 238 ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) // timeout of individual endorsement 239 defer cancel() 240 response, err := endorser.client.ProcessProposal(ctx, signedProposal) 241 done <- &ppResponse{response: response, err: err} 242 }() 243 select { 244 case resp := <-done: 245 // Endorser completedLayout normally 246 code, message, _, remove := responseStatus(resp.response, resp.err) 247 if code != codes.OK { 248 logger.Warnw("Endorse call to endorser failed", "MSPID", endorser.mspid, "endpoint", endorser.address, "error", message) 249 if remove { 250 gs.registry.removeEndorser(endorser) 251 } 252 plan.addError(errorDetail(endorser.endpointConfig, message)) 253 return false 254 } 255 response = resp.response 256 logger.Debugw("Endorse call to endorser returned success", "MSPID", endorser.mspid, "endpoint", endorser.address, "status", response.Response.Status, "message", response.Response.Message) 257 258 responseMessage := response.GetResponse() 259 if responseMessage != nil { 260 responseMessage.Payload = nil // Remove any duplicate response payload 261 } 262 263 return plan.processEndorsement(endorser, response) 264 case <-ctx.Done(): 265 // Overall endorsement timeout expired 266 return false 267 } 268 } 269 270 // planFromFirstEndorser implements the gateway's strategy of processing the proposal on a single (preferably local) peer 271 // and using the ChaincodeInterest from the response to invoke discovery and build an endorsement plan. 272 // Returns the endorsement plan which can be used to request further endorsements, if required. 273 func (gs *Server) planFromFirstEndorser(ctx context.Context, channel string, chaincodeID string, hasTransientData bool, signedProposal *peer.SignedProposal, logger *flogging.FabricLogger) (*plan, error) { 274 defaultInterest := &peer.ChaincodeInterest{ 275 Chaincodes: []*peer.ChaincodeCall{{ 276 Name: chaincodeID, 277 }}, 278 } 279 280 // 1. Choose an endorser from the gateway's organization 281 plan, err := gs.registry.planForOrgs(channel, chaincodeID, []string{gs.registry.localEndorser.mspid}) 282 if err != nil { 283 // No local org endorsers for this channel/chaincode. If transient data is involved, return error 284 if hasTransientData { 285 return nil, status.Error(codes.FailedPrecondition, "no endorsers found in the gateway's organization; retry specifying endorsing organization(s) to protect transient data") 286 } 287 // Otherwise, just let discovery pick one. 288 plan, err = gs.registry.endorsementPlan(channel, defaultInterest, nil) 289 if err != nil { 290 return nil, status.Error(codes.FailedPrecondition, err.Error()) 291 } 292 } 293 firstEndorser := plan.endorsers()[0] 294 295 gs.logger.Debugw("Sending to first endorser:", "MSPID", firstEndorser.mspid, "endpoint", firstEndorser.address) 296 297 // 2. Process the proposal on this endorser 298 var firstResponse *peer.ProposalResponse 299 var errDetails []proto.Message 300 301 for firstResponse == nil && firstEndorser != nil { 302 done := make(chan struct{}) 303 go func() { 304 defer close(done) 305 306 ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) 307 defer cancel() 308 firstResponse, err = firstEndorser.client.ProcessProposal(ctx, signedProposal) 309 code, message, _, remove := responseStatus(firstResponse, err) 310 311 if code != codes.OK { 312 logger.Warnw("Endorse call to endorser failed", "endorserAddress", firstEndorser.address, "endorserMspid", firstEndorser.mspid, "error", message) 313 errDetails = append(errDetails, errorDetail(firstEndorser.endpointConfig, message)) 314 if remove { 315 gs.registry.removeEndorser(firstEndorser) 316 } 317 firstEndorser = plan.nextPeerInGroup(firstEndorser) 318 firstResponse = nil 319 } 320 }() 321 select { 322 case <-done: 323 // Endorser completedLayout normally 324 case <-ctx.Done(): 325 // Overall endorsement timeout expired 326 logger.Warn("Endorse call timed out while collecting first endorsement") 327 return nil, newRpcError(codes.DeadlineExceeded, "endorsement timeout expired while collecting first endorsement") 328 } 329 } 330 if firstEndorser == nil || firstResponse == nil { 331 return nil, newRpcError(codes.Aborted, "failed to endorse transaction, see attached details for more info", errDetails...) 332 } 333 334 // 3. Extract ChaincodeInterest and SBE policies 335 // The chaincode interest could be nil for legacy peers and for chaincode functions that don't produce a read-write set 336 interest := firstResponse.Interest 337 if len(interest.GetChaincodes()) == 0 { 338 interest = defaultInterest 339 } 340 341 // 4. If transient data is involved, then we need to ensure that discovery only returns orgs which own the collections involved. 342 // Do this by setting NoPrivateReads to false on each collection 343 if hasTransientData { 344 for _, call := range interest.GetChaincodes() { 345 call.NoPrivateReads = false 346 } 347 } 348 349 // 5. Get a set of endorsers from discovery via the registry 350 // The preferred discovery layout will contain the firstEndorser's Org. 351 plan, err = gs.registry.endorsementPlan(channel, interest, firstEndorser) 352 if err != nil { 353 return nil, status.Error(codes.FailedPrecondition, err.Error()) 354 } 355 356 // 6. Remove the gateway org's endorser, since we've already done that 357 plan.processEndorsement(firstEndorser, firstResponse) 358 359 return plan, nil 360 } 361 362 // responseStatus unpacks the proposal response and error values that are returned from ProcessProposal and 363 // determines how the gateway should react (retry?, close connection?). 364 // Uses the grpc canonical status error codes and their recommended actions. 365 // Returns: 366 // - response status code, with codes.OK indicating success and other values indicating likely error type 367 // - error message extracted from the err or generated from 500 proposal response (string) 368 // - should the gateway retry (only the Evaluate() uses this) (bool) 369 // - should the gateway close the connection and remove the peer from its registry (bool) 370 func responseStatus(response *peer.ProposalResponse, err error) (statusCode codes.Code, message string, retry bool, remove bool) { 371 if err != nil { 372 if response == nil { 373 // there is no ProposalResponse, so this must have been generated by grpc in response to an unavailable peer 374 // - close the connection and retry on another 375 return codes.Unavailable, err.Error(), true, true 376 } 377 // there is a response and an err, so it must have been from the unpackProposal() or preProcess() stages 378 // preProcess does all the signature and ACL checking. In either case, no point retrying, or closing the connection (it's a client error) 379 return codes.FailedPrecondition, err.Error(), false, false 380 } 381 if response.Response.Status < 200 || response.Response.Status >= 400 { 382 if response.Payload == nil && response.Response.Status == 500 { 383 // there's a error 500 response but no payload, so the response was generated in the peer rather than the chaincode 384 if strings.HasSuffix(response.Response.Message, chaincode.ErrorStreamTerminated) { 385 // chaincode container crashed probably. Close connection and retry on another peer 386 return codes.Aborted, response.Response.Message, true, true 387 } 388 // some other error - retry on another peer 389 return codes.Aborted, response.Response.Message, true, false 390 } else { 391 // otherwise it must be an error response generated by the chaincode 392 return codes.Unknown, fmt.Sprintf("chaincode response %d, %s", response.Response.Status, response.Response.Message), false, false 393 } 394 } 395 // anything else is a success 396 return codes.OK, "", false, false 397 } 398 399 // Submit will send the signed transaction to the ordering service. The response indicates whether the transaction was 400 // successfully received by the orderer. This does not imply successful commit of the transaction, only that is has 401 // been delivered to the orderer. 402 func (gs *Server) Submit(ctx context.Context, request *gp.SubmitRequest) (*gp.SubmitResponse, error) { 403 if request == nil { 404 return nil, status.Error(codes.InvalidArgument, "a submit request is required") 405 } 406 txn := request.GetPreparedTransaction() 407 if txn == nil { 408 return nil, status.Error(codes.InvalidArgument, "a prepared transaction is required") 409 } 410 if len(txn.Signature) == 0 { 411 return nil, status.Error(codes.InvalidArgument, "prepared transaction must be signed") 412 } 413 orderers, err := gs.registry.orderers(request.ChannelId) 414 if err != nil { 415 return nil, status.Errorf(codes.FailedPrecondition, "%s", err) 416 } 417 418 if len(orderers) == 0 { 419 return nil, status.Errorf(codes.Unavailable, "no orderer nodes available") 420 } 421 422 // try each orderer in random order 423 var errDetails []proto.Message 424 for _, index := range rand.Perm(len(orderers)) { 425 orderer := orderers[index] 426 logger.Infow("Sending transaction to orderer", "txID", request.TransactionId, "endpoint", orderer.address) 427 err := gs.broadcast(ctx, orderer, txn) 428 if err == nil { 429 return &gp.SubmitResponse{}, nil 430 } 431 432 logger.Warnw("Error sending transaction to orderer", "txID", request.TransactionId, "endpoint", orderer.address, "err", err) 433 errDetails = append(errDetails, errorDetail(orderer.endpointConfig, err.Error())) 434 435 errStatus := toRpcStatus(err) 436 if errStatus.Code() != codes.Unavailable { 437 return nil, newRpcError(errStatus.Code(), errStatus.Message(), errDetails...) 438 } 439 } 440 441 return nil, newRpcError(codes.Unavailable, "no orderers could successfully process transaction", errDetails...) 442 } 443 444 func (gs *Server) broadcast(ctx context.Context, orderer *orderer, txn *common.Envelope) error { 445 broadcast, err := orderer.client.Broadcast(ctx) 446 if err != nil { 447 return err 448 } 449 450 if err := broadcast.Send(txn); err != nil { 451 return err 452 } 453 454 response, err := broadcast.Recv() 455 if err != nil { 456 return err 457 } 458 459 if response.GetStatus() != common.Status_SUCCESS { 460 return status.Errorf(codes.Aborted, "received unsuccessful response from orderer: %s", common.Status_name[int32(response.GetStatus())]) 461 } 462 463 return nil 464 } 465 466 // CommitStatus returns the validation code for a specific transaction on a specific channel. If the transaction is 467 // already committed, the status will be returned immediately; otherwise this call will block and return only when 468 // the transaction commits or the context is cancelled. 469 // 470 // If the transaction commit status cannot be returned, for example if the specified channel does not exist, a 471 // FailedPrecondition error will be returned. 472 func (gs *Server) CommitStatus(ctx context.Context, signedRequest *gp.SignedCommitStatusRequest) (*gp.CommitStatusResponse, error) { 473 if signedRequest == nil { 474 return nil, status.Error(codes.InvalidArgument, "a commit status request is required") 475 } 476 477 request := &gp.CommitStatusRequest{} 478 if err := proto.Unmarshal(signedRequest.Request, request); err != nil { 479 return nil, status.Errorf(codes.InvalidArgument, "invalid status request: %v", err) 480 } 481 482 signedData := &protoutil.SignedData{ 483 Data: signedRequest.Request, 484 Identity: request.Identity, 485 Signature: signedRequest.Signature, 486 } 487 if err := gs.policy.CheckACL(resources.Gateway_CommitStatus, request.ChannelId, signedData); err != nil { 488 return nil, status.Error(codes.PermissionDenied, err.Error()) 489 } 490 491 txStatus, err := gs.commitFinder.TransactionStatus(ctx, request.ChannelId, request.TransactionId) 492 if err != nil { 493 return nil, toRpcError(err, codes.Aborted) 494 } 495 496 response := &gp.CommitStatusResponse{ 497 Result: txStatus.Code, 498 BlockNumber: txStatus.BlockNumber, 499 } 500 return response, nil 501 } 502 503 // ChaincodeEvents supplies a stream of responses, each containing all the events emitted by the requested chaincode 504 // for a specific block. The streamed responses are ordered by ascending block number. Responses are only returned for 505 // blocks that contain the requested events, while blocks not containing any of the requested events are skipped. The 506 // events within each response message are presented in the same order that the transactions that emitted them appear 507 // within the block. 508 func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest, stream gp.Gateway_ChaincodeEventsServer) error { 509 if signedRequest == nil { 510 return status.Error(codes.InvalidArgument, "a chaincode events request is required") 511 } 512 513 request := &gp.ChaincodeEventsRequest{} 514 if err := proto.Unmarshal(signedRequest.Request, request); err != nil { 515 return status.Errorf(codes.InvalidArgument, "invalid chaincode events request: %v", err) 516 } 517 518 signedData := &protoutil.SignedData{ 519 Data: signedRequest.Request, 520 Identity: request.Identity, 521 Signature: signedRequest.Signature, 522 } 523 if err := gs.policy.CheckACL(resources.Gateway_ChaincodeEvents, request.ChannelId, signedData); err != nil { 524 return status.Error(codes.PermissionDenied, err.Error()) 525 } 526 527 ledger, err := gs.ledgerProvider.Ledger(request.GetChannelId()) 528 if err != nil { 529 return status.Error(codes.NotFound, err.Error()) 530 } 531 532 startBlock, err := startBlockFromLedgerPosition(ledger, request.GetStartPosition()) 533 if err != nil { 534 return err 535 } 536 537 ledgerIter, err := ledger.GetBlocksIterator(startBlock) 538 if err != nil { 539 return status.Error(codes.Aborted, err.Error()) 540 } 541 542 eventsIter := event.NewChaincodeEventsIterator(ledgerIter) 543 defer eventsIter.Close() 544 545 for { 546 response, err := eventsIter.Next() 547 if err != nil { 548 return status.Error(codes.Aborted, err.Error()) 549 } 550 551 var matchingEvents []*peer.ChaincodeEvent 552 553 for _, event := range response.Events { 554 if event.GetChaincodeId() == request.GetChaincodeId() { 555 matchingEvents = append(matchingEvents, event) 556 } 557 } 558 559 if len(matchingEvents) == 0 { 560 continue 561 } 562 563 response.Events = matchingEvents 564 565 if err := stream.Send(response); err != nil { 566 if err == io.EOF { 567 // Stream closed by the client 568 return status.Error(codes.Canceled, err.Error()) 569 } 570 return err 571 } 572 } 573 } 574 575 func startBlockFromLedgerPosition(ledger ledger.Ledger, position *ab.SeekPosition) (uint64, error) { 576 switch seek := position.GetType().(type) { 577 case nil: 578 case *ab.SeekPosition_NextCommit: 579 case *ab.SeekPosition_Specified: 580 return seek.Specified.GetNumber(), nil 581 default: 582 return 0, status.Errorf(codes.InvalidArgument, "invalid start position type: %T", seek) 583 } 584 585 ledgerInfo, err := ledger.GetBlockchainInfo() 586 if err != nil { 587 return 0, status.Error(codes.Aborted, err.Error()) 588 } 589 590 return ledgerInfo.GetHeight(), nil 591 }