Desenvolvimento

24 abr, 2017

Chaincode para desenvolvedores de Go – parte 02

Publicidade

Continuando de onde paramos na primeira parte desta série, vamos seguir o desenvolvimento do chaincode usando o Golang para uma rede de blockchain baseada em Hyperledger Fabric v0.6.

Armazenar e recuperar dados

O código na Listagem 7 e na Listagem 8 mostra como armazenar e buscar dados do ledger.

Armazenamento de dados no ledger

O método CreateLoanApplication na linha 1 da Listagem 7 apresenta dois argumentos. O primeiro é o ChaincodeStubInterface, que tem APIs úteis para interagir com o ledger blockchain, contexto de transação, certificados de callers, etc. O segundo argumento é uma string array que pode ser usada pelo invocador do método para passar os argumentos necessários.

As linhas 2-8 identificam o registro e a validação de argumentos de entrada.

Na linha 9, o valor de Id de aplicativo de empréstimo, que seria usado como a chave para armazenar o objeto do aplicativo de empréstimo real, é recuperado.

Na linha 10, o conteúdo da aplicação de empréstimo real é recuperado na forma de uma cadeia JSON. Por exemplo:

‘{“propertyId”:”prop1″,”landId”:”land1″,”permitId”:”permit1″,”buyerId”:”vojha24″,”personalInfo”:{“firstname”:”Varun”,”lastname”:”Ojha”,”dob”:”dob”,”email”:”varun@gmail.com”,”mobile”:”99999999″},”financialInfo”:{“monthlySalary”:10000,”otherExpenditure”:0,”monthlyRent”:1000,”monthlyLoanPayment”:1000},”status”:”Submitted”,”requestedAmount”:40000,”fairMarketValue”:58000,”approvedAmount”:40000,”reviewedBy”:”abc1″,”lastModifiedDate”:”21/09/2016 2:30pm”}’

A linha 12 é onde o método stub.PutState é invocado para armazenar o id do aplicativo de empréstimo e o conteúdo real do aplicativo de empréstimo JSON como um par de valores-chave no ledger de blockchain. Observe que o valor armazenado no armazenamento de valo-chave deve sempre ser uma array de bytes. Daí a sequência JSON do aplicativo de empréstimo ser primeiro convertida em uma array de bytes antes de armazená-la no ledger.

Listagem 7. Código para armazenar dados no ledger
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
 
    if len(args) < 2 {
        fmt.Println("Invalid number of args")
        return nil, errors.New("Expected at least two arguments for loan application creation")
    }
 
    var loanApplicationId = args[0]
    var loanApplicationInput = args[1]
 
    err := stub.PutState(loanApplicationId, []byte(loanApplicationInput))
    if err != nil {
        fmt.Println("Could not save loan application to ledger", err)
        return nil, err
    }
 
    fmt.Println("Successfully saved loan application")
    return nil, nil
}

Buscando dados do ledger

Na Listagem 8, o método GetLoanApplication na linha 1 apresenta dois argumentos. O primeiro é o ChaincodeStubInterface, que tem APIs úteis para interagir com o ledger blockchain, contexto de transação, certificados de callers, etc. O segundo argumento é uma string array que pode ser usada pelo invocador do método para passar os argumentos necessários.

As linhas 2-7 controlam o registro e a validação de argumentos de entrada.

Na linha 9, o valor de Id de aplicativo de empréstimo, que seria usado como a chave para recuperar o objeto do aplicativo de empréstimo real do ledger, é recuperado.

A linha 10 é onde o método stub.GetState é invocado para recuperar o conteúdo JSON do aplicativo de empréstimo na forma de uma array de bytes passando na chave loanApplicationId. Observe que o valor armazenado no armazenamento de valor-chave deve sempre ser uma array de bytes. Daí a sequência JSON do aplicativo de empréstimo ser primeiro convertida em uma array de bytes antes de armazená-la no ledger.

Listagem 8. Código para buscar dados do ledger
func GetLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering GetLoanApplication")
 
    if len(args) < 1 {
        fmt.Println("Invalid number of arguments")
        return nil, errors.New("Missing loan application ID")
    }
 
    var loanApplicationId = args[0]
    bytes, err := stub.GetState(loanApplicationId)
    if err != nil {
        fmt.Println("Could not fetch loan application with id "+loanApplicationId+" from ledger", err)
        return nil, err
    }
    return bytes, nil
}

Nota: Existe também uma maneira de lidar com dados usando modelos de dados relacionais tradicionais. Por exemplo, ChaincodeStubInterface tem uma maneira de criar tabelas e lidar com linhas e colunas. Mas isso é apenas uma abstração lógica e os dados serão armazenados como um par de valores-chave no RocksDB. No momento desta escrita, a versão 1.0 da Hyperledger Fabric está em desenvolvimento e depreciou a estrutura de dados da tabela. Portanto, este tutorial não discute isto a fim de minimizar as mudanças que um desenvolvedor de chaincode precisaria fazer de v0.6 para v1.0.

Estruturas golang ordenadas e não ordenadas para sequências JSON

Conforme demonstrado nas Listagens 7 e 8, os métodos stub.PutState e stub.GetState lidam somente com arrays de bytes. Portanto, é importante ser capaz de converter objetos estruturais regulares sendo usados no chaincode para sequências JSON e vice-versa.

A Listagem 9 mostra como ordenar uma estrutura em uma array de bytes de sequência JSON que pode ser armazenada no ledger. A linha 2 cria uma instância do objeto PersonalInfo. A linha 3 usa o pacote json para ordenar o objeto em uma sequência JSON e retornar a array de bytes para o mesmo.

O pacote json pode ser importado incluindo “encoding/json” no bloco de importação na parte superior. Esta array de bytes pode, então, ser armazenada no ledger usando o método stub.PutState demonstrado na Listagem 7.

Listagem 9. Código para ordenar uma estrutura em uma array de bytes de sequência JSON
var personalInfo PersonalInfo
 personalInfo = PersonalInfo{"Varun", "Ojha", "dob", "varun@gmail.com", "9999999999"}
 bytes, err := json.Marshal (&personalInfo)
 if err != nil {
        fmt.Println("Could not marshal personal info object", err)
        return nil, err
 }
 err = stub.PutState("key", bytes)

A Listagem 10 mostra como desordenar uma estrutura de uma array de bytes em uma estrutura preenchida. A linha 1 busca os bytes de sequência JSON do PersonalInfo a partir do ledger utilizando a chave associada. A linha 3 desordena os bytes recuperados na linha 1 no objeto PersonalInfo referenciado pela variável personalInfo.

Agora você pode acessar e modificar o objeto personalInfo usando a notação dot como mostrado na linha 4.

Listagem 10. Código para desordenar uma estrutura de uma array de bytes em uma estrutura preenchida
piBytes, err := stub.GetState("la1")    
 var personalInfo PersonalInfo
 err = json.Unmarshal(piBytes, &personalInfo)
 fmt.Println(personalInfo.Firstname)

Implementando controle de acesso e permissões

Uma das principais diferenças entre Hyperledger e outros Fabrics de blockchain é que ele tem um ledger seguro e licenciado, que é adequado para a implementação de soluções de nível empresarial.

Serviços de associação

O componente de serviços de Associação no Hyperledger desempenha um papel fundamental na habilitação de segurança e permissões dentro da rede blockchain. É responsável pela emissão de certificados de Inscrição e Transação para os usuários em resposta ao registro e inscrição.

  • Certificado de Inscrição: A autoridade de certificação nos serviços de associação emitirá um certificado de inscrição para um usuário que deseja transacionar em blockchain como prova de identidade.
  • Certificado de Transação: O certificado de transação deveria ser usado como um token único que é passado ao longo de cada invocação de requisição do chaincode pelo aplicativo que invoca/é invocado. Os certificados de transação são matematicamente derivados do certificado de inscrição pai e, portanto, um número teoricamente ilimitado de certificados de transação podem ser gerados a partir do certificado de inscrição pai. O certificado de transação pode ser usado para assinar e criptografar os dados da transação quando ele é armazenado no blockchain. Isso garante que somente o usuário com o certificado de transação ou um regulador/auditor etc. que tenha o certificado pai possa realmente visualizar os conteúdos da transação uma vez que eles foram escritos.
  • Atributos: Cada certificado de transação pode conter vários atributos definidos pelo usuário. Estes podem ser mencionados como parte do pedido de registro para a autoridade de certificação a partir do aplicativo cliente. Tutoriais subsequentes nesta série tratarão disso em detalhes a partir da perspectiva de desenvolver o aplicativo cliente.

A Listagem 11 mostra como os atributos podem ser recuperados do certificado de transação do chamador. Conforme mencionado anteriormente, o usuário ou o aplicativo cliente precisará passar no certificado do usuário como parte de cada solicitação de chamada de chaincode para autenticar com o ponto de destino na rede de blockchain. O SDK do HFC cuida de passar o certificado como parte do pedido automaticamente.

A função Invoke na linha 11 da Listagem 11 foi modificada para verificar o nome da função de entrada e delegar a chamada para a função de manipulador apropriada. Além disso, a função Invoke também valida o acesso e a função do caller que enviou o pedido de invocação CreateLoanApplication. A função Invoke chama a função GetCertAttribute personalizada para recuperar um determinado atributo do certificado de transação do chamador.

A função GetCertAttribute busca o valor do atributo passando o nome do atributo na linha 3.

A linha 15 verifica se o chamador tem a função de um Administrador do Banco e pode invocar a função CreateLoanApplication. Se o chamador não tiver a função necessária, um erro apropriado é retornado. Desta forma, o controle de acesso baseado em atributos pode ser implementado no chaincode.

O ChaincodeStubInterface tem algumas funções de utilidade que tratam de atributos, como ReadCertAttribute, VerifyAttribute e VerifyAttributes. Todos esses métodos dependem do pacote github.com/hyperledger/fabric/core/chaincode/shim/crypto/attr para criar e usar o AttributeHandlerImpl que manipula atributos.

Na versão do Hyperledger Fabric que está atualmente em desenvolvimento (v1.0), essas funções de utilidade foram removidas do ChaincodeStubInterface. Daí na v1.0, o desenvolvedor de chaincode precisaria usar o AttributeHandlerImpl diretamente para trabalhar com atributos.

Listagem 11. Recuperando atributos do certificado de transação do chamador
func GetCertAttribute(stub shim.ChaincodeStubInterface, attributeName string) (string, error) {
    fmt.Println("Entering GetCertAttribute")
    attr, err := stub.ReadCertAttribute(attributeName)
    if err != nil {
        return "", errors.New("Couldn't get attribute " + attributeName + ". Error: " + err.Error())
    }
    attrString := string(attr)
    return attrString, nil
}
 
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    if function == "CreateLoanApplication" {
        username, _ := GetCertAttribute(stub, "username")
        role, _ := GetCertAttribute(stub, "role")
        if role == "Bank_Home_Loan_Admin" {
            return CreateLoanApplication(stub, args)
        } else {
            return nil, errors.New(username + " with role " + role + " does not have access to create a loan application")
        }
 
    }
    return nil, nil
}

Criação e emissão de eventos personalizados

O Hyperledger contém um framework de eventos que pode ser usado para publicar/assinar eventos predefinidos ou personalizados. Eventos personalizados podem ser criados no chaincode e emitidos a seu critério. Por exemplo, um evento poderia ser gerado sempre que houver qualquer alteração no estado do blockchain. Esses eventos podem ser assinados para e consumidos pelo aplicativo cliente registrando um adaptador de eventos com o hub de eventos em blockchain. Os tutoriais subsequentes nesta série mostrarão em detalhes como o aplicativo cliente pode se inscrever e consumir eventos produzidos através do chaincode usando o SDK do HFC.

Além dos eventos personalizados, alguns dos eventos internos predefinidos que fazem parte do Hyperledger são:

  • Bloquear eventos
  • Eventos Chaincode
  • Eventos de rejeição
  • Eventos de registro

O código na Listagem 12 mostra como criar e publicar um evento personalizado.

As linhas 1-4 definem um objeto de evento personalizado contendo campos de digitação e descrição.

A função CreateLoanApplication começa na linha 6 e foi modificada para incluir a criação do evento na criação bem-sucedida do aplicativo de empréstimo.

A linha 23 cria a instância do objeto customEvent e preenche detalhes de eventos apropriados.

A linha 24 ordena o objeto de evento para uma array de bytes de sequência JSON, conforme explicado anteriormente.

A linha 28 define o evento personalizado. O método stub.SetEvent apresenta dois argumentos: o nome do evento e a carga útil. O aplicativo cliente pode se inscrever no mesmo nome/tópico do evento para receber eventos enquanto e quando eles são gerados pelo chaincode.

Listagem 12. Criando e publicando um evento personalizado
type customEvent struct {
    Type        string `json:"type"`
    Description string `json:"description"`
}
 
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
 
    if len(args) < 2 {
        fmt.Println("Invalid number of args")
        return nil, errors.New("Expected at least two arguments for loan application creation")
    }
 
    var loanApplicationId = args[0]
    var loanApplicationInput = args[1]
 
    err := stub.PutState(loanApplicationId, []byte(loanApplicationInput))
    if err != nil {
        fmt.Println("Could not save loan application to ledger", err)
        return nil, err
    }
 
    var event = customEvent{"createLoanApplication", "Successfully created loan application with ID " + loanApplicationId}
    eventBytes, err := json.Marshal(&event)
    if err != nil {
            return nil, err
    }
    err = stub.SetEvent("evtSender", eventBytes)
    if err != nil {
        fmt.Println("Could not set event for loan application creation", err)
    }
 
    fmt.Println("Successfully saved loan application")
    return nil, nil
 
}

Manipulação de registro

O registro pode ser tratado no chaincode tanto usando o pacote fmt padrão e instruções de impressão quanto usando a digitação ChaincodeLogger no pacote shim.

O ChaincodeLogger suporta os seguintes níveis de log:

  • CRÍTICO
  • ERRO
  • ATENÇÃO
  • AVISO
  • INFO
  • DEPURAR

Você pode definir o nível de log de três maneiras:

  • SetChaincodeLoggingLevel (): este método irá pegar o nível de registro especificado no arquivo core.yaml no conjunto CORE_LOGGING_CHAINCODE. O arquivo core.yaml contém todas as informações de configuração necessárias para configurar e implantar a rede blockchain.
  • SetLoggingLevel (level LoggingLevel): este método irá definir o nível de registro no nível do shim.
  • SetLevel (level LoggingLevel): este método definirá o nível de registro no nível de instância do registrador individual.

A Listagem 13 mostra como criar, configurar e usar o ChaincodeLogger.

Listagem 13. Criando, configurando e usando o ChaincodeLogger
func SampleLogging() {
        //Different Logging Levels
        criticalLevel, _ := shim.LogLevel("CRITICAL")
        errorLevel, _ := shim.LogLevel("ERROR")
        warningLevel, _ := shim.LogLevel("WARNING")
        noticeLevel, _ := shim.LogLevel("NOTICE")
        infoLevel, _ := shim.LogLevel("INFO")
        debugLevel, _ := shim.LogLevel("DEBUG")
 
        //Logging level at the shim level
        shim.SetLoggingLevel(infoLevel)
 
        //Create a logger instance
        myLogger := shim.NewLogger("SampleChaincodeLogger")
 
        //Set logging level on logger instance
        myLogger.SetLevel(infoLevel)
 
        //Check logging level
        fmt.Println(myLogger.IsEnabledFor(infoLevel))
 
        //Log statements
        myLogger.Info("Info Message")
        myLogger.Critical("Critical Message")
        myLogger.Warning("Warning Message")
        myLogger.Error("Error Message")
        myLogger.Notice("Notice Message")
        myLogger.Debug("Debug Message")
 
    }

Perguntas frequentes e melhores práticas

Enquanto desenvolvo aplicativos de blockchain com clientes, muitas vezes respondo a essas perguntas.

– Como armazeno arquivos (imagens, áudio, vídeo, PDF, etc.) em blockchain?

Ambas as abordagens funcionam no atual Hyperledger Fabric (v0.6):

  • Armazene todos os arquivos/objetos como sequências codificadas em base64. O aplicativo cliente converteria o arquivo/objeto em uma sequência codificada em base64 e o enviaria como um parâmetro de entrada para uma função chaincode. O chaincode, por sua vez, pode armazená-lo como uma array de bytes no armazenamento chave/de valor.
  • Armazene o conteúdo real do arquivo/objeto fora de blockchain; por exemplo, no serviço IBM Bluemix Object Storage. Armazene apenas o link/referência/ID do arquivo/objeto em blockchain juntamente com o hash do arquivo/objeto. Armazenar o hash garante que qualquer manipulação com o arquivo/objeto fora de blockchain pode ser detectada por partes/entidades interessadas.

– Como posso evitar a divulgação de detalhes da lógica/contrato comercial privado para todos os pares na rede?

Esta questão surgiu em um cenário de cadeia de suprimentos onde um consumidor final da solução de blockchain não estava confortável compartilhando informações de contrato/lógica de negócio privado (como diferentes taxas negociadas com fornecedores diferentes) no contrato inteligente visível para todos os pares. Na v0.6, esta situação pode ser resolvida através da integração de sistemas externos.

A solução: A lógica/regras/contrato de negócio que o par quer manter privado pode ser executada como um conjunto de regras de negócios em um aplicativo externo como um serviço. O chaincode em si tem a capacidade de fazer chamadas de saída. Assim, o chaincode poderia fazer chamadas REST API, por exemplo, para o serviço de regras/lógica de negócio e buscar os resultados, mantendo assim a lógica escondida do chaincode real.

É possível integrar com sistemas externos ao blockchain a partir do chaincode. Por exemplo, o chaincode pode ser usado para conversar com bancos de dados externos, APIs etc. Mas é importante garantir que a interação com esses sistemas não torne o chaincode não determinístico.

Restrições e preocupações

  • O serviço de regras de negócios pode ser modificado sem o conhecimento dos pares restantes, uma vez que ele está sendo executado fora de blockchain. Dependendo do tipo de interação de negócios entre os diferentes participantes na rede blockchain, isso pode levar a problemas de confiança.
  • O serviço de regras de negócios deve estar disponível para todos os pares na rede blockchain que executarão o contrato inteligente/chaincode.
  • A solução pode levar a chaincode tornando-se não determinístico. Chaincode DEVE ser determinístico. Em poucas palavras, se a mesma função no chaincode é invocada com os mesmos parâmetros por várias partes, os resultados devem ser os mesmos. Por exemplo, se você usar um carimbo de data/hora ou um contador em suas funções chaincode que estão vinculadas à resposta, invocações do chaincode por vários pares levarão a resultados diferentes. Isto conduzirá a um estado inconsistente de ledger entre os diferentes pares na rede blockchain.

Lembre-se de que cada invocação do chaincode faz com que todos os pares que fazem parte da rede de consenso invoquem o chaincode em suas cópias locais do ledger.

Nota: Na v1.0 do Hyperledger Fabric que está atualmente em desenvolvimento, este problema foi resolvido organicamente através de mudanças na própria arquitetura.

Conclusão

Este tutorial começou com os fundamentos do chaincode e, em seguida, mergulhou nos blocos de construção e APIs diferentes disponíveis para executar tarefas importantes no chaincode, tais como controle de acesso, modelagem de dados e gerenciamento de eventos. Faça o download dos snippets de código consolidados no arquivo SampleChaincode.go.

A partir daqui veremos o TDD do chaincode. A parte final mostrará como desenvolver aplicativos cliente Node.js que podem conversar com a rede blockchain e completar a história de desenvolvimento.

Recursos para download

 

***

Varun Ojha faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://www.ibm.com/developerworks/cloud/library/cl-ibm-blockchain-chaincode-development-using-golang/index.html?ca=drs-