Desenvolvimento

2 mai, 2017

Chaincode para desenvolvedores de Go – parte 04

Publicidade

Esta é a quarta e última parte do artigo Chaincode para desenvolvedores Go. Se você não leu os artigos anteriores, leia aqui a parte 01, a parte 02 e a parte 03.

Nas duas primeiras partes desta série, falamos dos fundamentos, como o papel do chaincode e das APIs para interagir com o Fabric subjacente, bem como tópicos avançados como modelagem de dados, controle de acesso e eventos.

Na terceira parte vimos os conceitos de TDD e como aplicar esta abordagem para escrever chaincode em Golang para Hyperledger Fabric v0.6. Daremos continuidade a isso hoje.

Invoque a implementação do método

Vamos usar o desenvolvimento test-driven para implementar o método shim.Chaincode.Invoke. O método Invoke é chamado pela infraestrutura chaincode, que passa na instância apropriada do ChaincodeStubInterface, juntamente com o nome da função e argumentos passados pelo invoker do chaincode (aplicativo cliente).

Requisitos

  1. O método Invoke deveria verificar o argumento de nome da função de entrada e delegar a execução ao manipulador apropriado.
  2. O método Invoke deveria retornar um erro no caso de um nome de função de entrada inválido.
  3. O método Invoke deveria implementar/delegar controle de acesso e gerenciamento de permissões com base no certificado de transação do chamador/invocador do chaincode. Somente o Bank_Admin deveria ser autorizado a invocar o método CreateLoanApplication.

O primeiro teste validará a funcionalidade descrita no Requisito 3 acima.

Lista 14. Trecho de código para o teste TestInvokeValidation
func TestInvokeValidation(t *testing.T) {
    fmt.Println("Entering TestInvokeValidation")
 
    attributes := make(map[string][]byte)
    attributes["username"] = []byte("vojha24")
    attributes["role"] = []byte("client")
 
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    _, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{loanApplicationID, loanApplication})
    if err == nil {
        t.Fatalf("Expected unauthorized user error to be returned")
    }
 
}

Conforme explicado na parte 01 e na parte 02, o certificado de transação do caller/invoker do chaincode pode conter atributos definidos pelo usuário. Esses atributos desempenham um papel fundamental na imposição de controle de acesso e permissões dentro do chaincode.

As linhas 5 e 6 adicionam os atributos de nome de usuário e função, que são, então, passados para o constructor CustomMockStub. Esses atributos devem ajudar a simular os atributos que podem ser recuperados do certificado de transação do caller/invoker do chaincode.

A linha 13 usa o método stub.MockInvoke para simular como o método shim.Chaincode.Invoke seria chamado em runtime pela infraestrutura chaincode diretamente.

O método MockInvoke recebe um ID de transação (que deveria ser gerado pela infraestrutura Blockchain no tempo de execução), o nome da função e argumentos de entrada.

Execute o conjunto de testes novamente. O teste TestInvokeValidation irá falhar conforme esperado. Este é o estágio Vermelho.

1 --- FAIL: TestInvokeValidation (0.00s)
2 sample_chaincode_test.go:158 Expected unauthorized user error to be returned
3 FAIL
4 exit status 1

Agora escreva o mínimo de código no método Invoke em sample_chaincode.go para fazer passar este teste. Este é o estágio Verde.

Listagem 15. Mínimo de código no método Invoke em sample_chaincode.go para passar no teste
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    fmt.Println("Entering Invoke")
    return nil, errors.New("unauthorized user")
}

Agora execute o conjunto de testes. O teste TestInvokeValidation passará.

1 Entering TestInvokeValidation
2 2017/03/06 23:22:27 MockStub( mockStub &{} )
3 Entering Invoke
4 PASS

O próximo teste passará no papel correto como Bank_Admin e esperará que o teste passe.

Listagem 16. Trecho de código para TestInvokeValidation2
func TestInvokeValidation2(t *testing.T) {
    fmt.Println("Entering TestInvokeValidation")
 
    attributes := make(map[string][]byte)
    attributes["username"] = []byte("vojha24")
    attributes["role"] = []byte("Bank_Admin")
 
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    _, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{loanApplicationID, loanApplication})
    if err != nil {
        t.Fatalf("Expected CreateLoanApplication to be invoked")
    }
 
}

Execute o conjunto de testes. O teste TestInvokeValidation2 irá falhar conforme esperado. Para que esse teste passe, devemos agora refatorar o código em Invoke em sample_chaincode.go.

Listagem 17. Refatorar o código do método Invoke em sample_chaincode.go
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    fmt.Println("Entering Invoke")
     
    ubytes, _ := stub.ReadCertAttribute("username")
    rbytes, _ := stub.ReadCertAttribute("role")
 
    username := string(ubytes)
    role := string(rbytes)
 
    if role != "Bank_Admin" {
        return nil, errors.New("caller with " + username + " and role " + role + " does not have 
         access to invoke CreateLoanApplication")
    }
    return nil, nil
}

Agora execute o conjunto de testes. O teste TestInvokeValidation2 passará.

Listagem 18. Código para testar a funcionalidade descrita nos Requisitos 1 e 2
func TestInvokeFunctionValidation(t *testing.T) {
    fmt.Println("Entering TestInvokeFunctionValidation")
 
    attributes := make(map[string][]byte)
    attributes["username"] = []byte("vojha24")
    attributes["role"] = []byte("Bank_Admin")
 
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    _, err := stub.MockInvoke("t123", "InvalidFunctionName", []string{})
    if err == nil {
        t.Fatalf("Expected invalid function name error")
    }
 
}

A linha 14 valida se uma mensagem de erro apropriada é retornada de Invoke.

Execute o teste TestInvokeFunctionValidation. Ele falhará conforme o esperado com o seguinte resultado:

1 --- FAIL: TestInvokeFunctionValidation (0.00s)
2 sample_chaincode_test.go:117 Expected invalid function name error
3 FAIL
4 exit status 1

Agora, vamos passar para o estágio Verde e escrever a quantidade mínima de código para fazer passar este teste. Atualize o método Invoke em sample_chaincode.go com este trecho de código:

Listagem 19. Adição mínima ao método Invoke em sample_chaincode.go para passar no teste
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    fmt.Println("Entering Invoke")
 
    ubytes, _ := stub.ReadCertAttribute("username")
    rbytes, _ := stub.ReadCertAttribute("role")
 
    username := string(ubytes)
    role := string(rbytes)
 
    if role != "Bank_Admin" {
        return nil, errors.New("caller with " + username + " and role " + role + " does not have access to invoke CreateLoanApplication")
    }
 
    return nil, errors.New("Invalid function name")
}

Execute o teste TestInvokeFunctionValidation novamente. Ele passará, uma vez que o método Invoke retornará o erro conforme o esperado. Mas, como discutido anteriormente, você precisaria refatorar este código após o próximo teste.

O próximo teste passará no nome da função correta CreateLoanApplication e esperará que a função seja invocada. Este trecho de código mostra o teste TestInvokeFunctionValidation2.

Listagem 20. Código para o teste TestInvokeFunctionValidation2
func TestInvokeFunctionValidation2(t *testing.T) {
    fmt.Println("Entering TestInvokeFunctionValidation2")
 
    attributes := make(map[string][]byte)
    attributes["username"] = []byte("vojha24")
    attributes["role"] = []byte("Bank_Admin")
 
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    _, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{})
    if err != nil {
        t.Fatalf("Expected CreateLoanApplication function to be invoked")
    }
 
}

Execute o teste TestInvokeFunctionValidation2. Ele falhará, como esperado.

1 Entering TestInvokeFunctionValidation2
2 2017/03/06 20:50:12 MockStub( mockStub &{} )
3 Entering Invoke 
4 --- FAIL: TestInvokeFunctionValidation2 (0.00s)
5 sample_chaincode_test.go:133 Expected CreateLoanApplication function to be
invoked
6 FAIL

Agora refatore o método Invoke em sample_chaincode.go para manipular a delegação da invocação de função.

Listagem 21. Refatore o método Invoke em sample_chaincode.go
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    fmt.Println("Entering Invoke")
 
    ubytes, _ := stub.ReadCertAttribute("username")
    rbytes, _ := stub.ReadCertAttribute("role")
 
    username := string(ubytes)
    role := string(rbytes)
 
    if role != "Bank_Admin" {
        return nil, errors.New("caller with " + username + " and role " + role + " does not have access to invoke CreateLoanApplication")
    }
     
    if function == "CreateLoanApplication" {
        return CreateLoanApplication(stub, args)
    }
    return nil, errors.New("Invalid function name. Valid functions ['CreateLoanApplication']")
}

Agora refatore o teste TestInvokeFunctionValidation2 para validar se o método CreateLoanApplication realmente foi invocado. Idealmente, isso deveria ser feito usando um spy object, que está disponível nas bibliotecas mock padrão, mas, por uma questão de simplicidade, este teste verifica a saída retornada do método Invoke para garantir que o método CreateLoanApplication foi realmente invocado.

Listagem 22. Refatore o teste TestInvokeFunctionValidation2
func TestInvokeFunctionValidation2(t *testing.T) {
    fmt.Println("Entering TestInvokeFunctionValidation2")
 
    attributes := make(map[string][]byte)
    attributes["username"] = []byte("vojha24")
    attributes["role"] = []byte("Bank_Admin")
 
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    bytes, err := stub.MockInvoke("t123", "CreateLoanApplication", []string{loanApplicationID, loanApplication})
    if err != nil {
        t.Fatalf("Expected CreateLoanApplication function to be invoked")
    }
    //A spy could have been used here to ensure CreateLoanApplication method actually got invoked.
    var la LoanApplication
    err = json.Unmarshal(bytes, &la)
    if err != nil {
        t.Fatalf("Expected valid loan application JSON string to be returned from CreateLoanApplication method")
    }
 
}

Agora execute o pacote de teste novamente. O teste TestInvokeFunctionValidation2 irá passar.

Testando funções não determinísticas

Conforme detalhado na Parte 2 desta série de tutoriais, o chaincode deve ser determinístico. Aqui está um exemplo para ilustrar. Tome uma rede de blockchain de quatro pares baseada em Hyperledger Fabric em que todos os quatro pares estão validando pares. Isso significa que sempre que uma transação precisa ser escrita no blockchain, todos os quatro pares irão executar a transação independentemente em suas cópias locais do ledger. Simplificando, cada um dos quatro pares executará a mesma função chaincode com a mesma entrada independentemente para atualizar seu estado de ledger local. Desta forma, todos os quatro pares terminam com o mesmo estado de ledger.

Assim, é necessário que todas as quatro execuções do chaincode pelos pares produzam o mesmo resultado para que eles acabem com o mesmo estado do ledger. Isso é chamado de chaincode determinístico.

A Listagem 23 demonstra uma versão não determinística da função CreateLoanApplication.

Isto significa que execuções múltiplas desta função com a mesma entrada conduzirão a resultados diferentes.

Listagem 23. Uma versão não determinística da função CreateLoanApplication
func NonDeterministicFunction(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering NonDeterministicFunction")
    //Use random number generator to generate the ID
    var random = rand.New(rand.NewSource(time.Now().UnixNano()))
    var loanApplicationID = "la1" + strconv.Itoa(random.Intn(1000))
    var loanApplication = args[0]
    var la LoanApplication
    err := json.Unmarshal([]byte(loanApplication), &la)
    if err != nil {
        fmt.Println("Could not unmarshal loan application", err)
        return nil, err
    }
    la.ID = loanApplicationID
    laBytes, err := json.Marshal(&la)
    if err != nil {
        fmt.Println("Could not marshal loan application", err)
        return nil, err
    }
    err = stub.PutState(loanApplicationID, laBytes)
    if err != nil {
        fmt.Println("Could not save loan application to ledger", err)
        return nil, err
    }
 
    fmt.Println("Successfully saved loan application")
    return []byte(loanApplicationID), nil
}

Ao contrário do método CreateLoanApplication original, que passou no ID do aplicativo de empréstimo como parte da entrada, o método acima usa um gerador de números aleatórios para gerar o ID e o anexa ao conteúdo do aplicativo de empréstimo. As linhas 4 e 5 demonstram como o ID do aplicativo de empréstimo está sendo gerado. A linha 19 armazena o conteúdo do aplicativo de empréstimo atualizado no ledger.

A Lista 24 mostra como testar se um método é não determinístico.

Listagem 24. Testando se um método é não determinístico

func TestNonDeterministicFunction(t *testing.T) {
    fmt.Println("Entering TestNonDeterministicFunction")
    attributes := make(map[string][]byte)
    const peerSize = 4
    var stubs [peerSize]*shim.CustomMockStub
    var responses [peerSize][]byte
    var loanApplicationCustom = `{"propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":16000,"otherExpenditure":0,"monthlyRent":4150,"monthlyLoanPayment":4000},"status":"Submitted","requestedAmount":40000,"fairMarketValue":58000,"approvedAmount":40000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}`
    //Simulate execution of the chaincode function by multiple peers on their local ledgers
    for j := 0; j < peerSize; j++ {
        stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
        if stub == nil {
            t.Fatalf("MockStub creation failed")
        }
        stub.MockTransactionStart("tx" + string(j))
        resp, err := NonDeterministicFunction(stub, []string{loanApplicationCustom})
        if err != nil {
            t.Fatalf("Could not execute NonDeterministicFunction ")
        }
        stub.MockTransactionEnd("tx" + string(j))
        stubs[j] = stub
        responses[j] = resp
    }
 
    for i := 0; i < peerSize; i++ {
        if i < (peerSize - 1) {
            la1Bytes, _ := stubs[i].GetState(string(responses[i]))
            la2Bytes, _ := stubs[i+1].GetState(string(responses[i+1]))
            la1 := string(la1Bytes)
            la2 := string(la2Bytes)
            if la1 != la2 {
                //TODO: Compare individual values to find mismatch
                t.Fatalf("Expected all loan applications to be identical. Non Deterministic chaincode error")
            }
        }
        //All loan applications retrieved from each of the peer's ledger's match. Function is deterministic
 
    }
 
}

A linha 4 define o número de pares de validação que queremos simular.

A linha 6 cria stubs que correspondem ao tamanho de pares de validação. Cada stub será usado para executar a função chaincode e atualizar seu estado do ledger.

As linhas 9 a 22 executam a função chaincode com o mesmo argumento de entrada usando os stubs criados anteriormente para simular como a função chaincode seria executada em um cenário real pelos pares de validação.

A linha 21 armazena a resposta para cada execução da função de chaincode. Nesse caso, a função que está sendo evocada é chamada de NonDeterministicFunction, que retorna o ID do aplicativo de empréstimo armazenado no ledger.

As linhas 25 a 38 usam os esboços criados anteriormente e os IDs de aplicativo de empréstimo retornados das execuções de função chaincode individuais para recuperar os aplicativos de empréstimo dos respectivos ledgers e compará-los para igualdade.

Para uma função determinística, esses aplicativos de empréstimo deveriam ter sido idênticos.

Agora execute o teste usando o go test. O teste TestNonDeterministicFunction falhará como esperado.

Uma vez que o NonDeterministicFunction usa um gerador de números aleatórios para gerar o ID do aplicativo de empréstimo, múltiplas invocações desta função levará a IDs diferentes. Assim, quando o aplicativo de empréstimo é finalmente salvo para os ledgers de pares individuais, o conteúdo do aplicativo de empréstimo será diferente, levando a estados de ledger inconsistentes entre os pares de validação.

Conclusão

Você já viu como fazer o desenvolvimento test-driven de chaincode implementando os métodos CreateLoanApplication e Invoke usando a abordagem TDD. O tutorial percorreu as etapas para usar o pacote de teste padrão de go para escrever testes de unidade e criar um esboço de simulação personalizado estendendo a implementação MockStub padrão no pacote shim para atender às suas necessidades de teste. Finalmente, você viu como uma função pode se tornar não determinística e como testar essa função durante o 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-testing-using-golang/index.html