In the early days of Hyperledger Fabric, there were many discussions about how to best implement smart contracts. Various contributors had been working on customer engagements / their own projects and had found deficiencies in the EVM (for good reason since although the EVM is Turing complete with is op codes, it relies on gas and accounts as constructs and does make it difficult to do complex data modeling, etc).
Rather than attempt to create a new contract language and execution language, we decided that we should attempt to allow people to bring to bear the full power of a programming languages of their choosing (today Golang is fully supported, Java is experimental and JavaScript is coming in the next release).
Now in order to allow polyglot contracts and not create complex embedded interpreters, etc, we chose to compile and run chaincode out of process. We then needed to figure out a way to "isolate" (and manage to a degree) these external processes and using Docker seemed to be a very viable option.
A few things to note:
- the chaincode processes / containers only communicate with the peer process (i.e. they initiate communication with the peer and do not expose an endpoint of their own)
- chaincode actually passes commands to the peer (i.e. when you do a Get/Put state, this is actually sent over a secure gRPC connection to the peer)
- the peer actually manages / isolates each chaincode invocation from other invocations (meaning that one invoke cannot access any state from another invoke)
- Hyperledger Fabric itself provides a mechanism whereby chaincode must actually be installed by the administrator of the peer (this is a benefit of running a permissioned blockchain). So before any chaincode is actually installed, the adminstrator / owner of the peer can inspect the chaincode and ensure that it is not doing anything malicious.
When it comes to non-determinism, a few things were investigated. For example, we've looked at whitelisting certain functions for various languages. But in the end, with the v1.0 architecture ensuring non-determinism on a per chaincode invocation basis is not actually required.
In the v1.0 model, each node executes chaincode and the output is actually a read/write set for the state accesses/changes. If the chaincode logic is successfully executed, the peer then signs the response. This is called simulation and endorsement. The number of endorsements (i.e. peers which must successfully run the chaincode) is configurable via policy. Once enough endorsements have been collected for a transaction, they are packages and submitted to the ordering services which then creates blocks of ordered transactions and delivers them to the peers for validation and commitment. This logic is 100% deterministic (check that the transaction is well-formed, ensure that there are enough endorsements to meet the policy and finally do classic MVCC checks to avoid collisions (aka "double-spend").
Hopefully this helps