Run sgx-pytorch on Alibaba Cloud Linux release 3 / CentOS 8

Refer to:

I highlight the differences.

1. Preinstall

Please follow Intel SGX to install Intel SGX (Please Make sure your local device and cloud device can support Intel SGX and FLC by hardware), and setup the DCAP server.

DCAP server allows you to do remote attestation on your hosted server, without connecting to the external Intel service.

**# Remove Python 3.6
sudo yum remove python3 python3-pip
# Install Python 3.8
sudo yum install python38 python38-pip python38-devel**

# Check Python and Pip version, ensure you are using Python 3.8
which python
pip --version

**pip install glog
# sgx-pytorch version 1.8.0 requires torchvision version 0.9.0
# https://pypi.org/project/torchvision/
pip install torchvision==0.9.0**
pip install astunparse numpy ninja pyyaml mkl mkl-include setuptools cmake cffi typing_extensions future six requests dataclasses setuptools_rust pycryptodome cryptography

git clone https://github.com/intel/pytorch -b sgx

**# I encounter network error even after setting up GitHub proxy like ghproxy.com
# Submodule is ok, but those subsubmodule with depth>1 will throw network error
# Finally, I setup Clash and fix the error. https://github.com/juewuy/ShellClash**
**git submodule sync && git submodule update --init --recursive**

2. Compile and start the key service on the key server side (local deployment by the model owner):

cd enclave_ops/deployment
make
cd bin/dkeyserver
sudo ./dkeyserver

The key server starts and waits for the key request from the dkeycache deployed on the SGX node. This service has two built-in model keys as test keys, and users can update and maintain them according to actual applications.

3. Compile and start the local key distribution service on public cloud side (cloud server deployment):

cd enclave_ops/deployment
make
cd bin/dkeycache
sudo ./dkeycache

After key cache service is started, this service will obtain all model keys. This service get key through SGX Remote Attestation, and sends the key to PyTorch with SGX’s enclave through SGX Local Attestation.

4. Compile PyTorch with SGX on public cloud side (cloud server deployment)

4.1 Compile oneDNN used by enclave

**# Set PyTorch_ROOT
PyTorch_ROOT=/opt/sgx-pytorch**

**# On Aliyun Linux, set SGXSDK_ROOT
SGXSDK_ROOT=/opt/alibaba/teesdk/intel/sgxsdk

source ${SGXSDK_ROOT}/environment**

cd ${PyTorch_ROOT}/third_party/sgx/linux-sgx
git am ../0001*
cd external/dnnl
make
sudo cp sgx_dnnl/lib/libsgx_dnnl.a ${SGXSDK_ROOT}/lib64/libsgx_dnnl2.a
sudo cp sgx_dnnl/include/* ${SGXSDK_ROOT}/include/

4.2 Compile enclave used by PyTorch

source ${SGXSDK_ROOT}/environment
cd ${PyTorch_ROOT}/enclave_ops/ideep-enclave
make

The enclave of PyTorch with SGX provides model parameter decryption and reference calculations. Note: There are 8 logical processors by default in Enclave/Enclave.config.xml. If the actual machine is greater than 8, the needs to update manually.

4.3 Compile PyTorch

cd ${PyTorch_ROOT}
pip uninstall torch (uninstall the Pytorch installed on the system, the self-compiled Pytorch will be installed)
source ${SGXSDK_ROOT}/environment

**# Compile CPU version, if no GPU on the server**
**USE_CUDA=0 python setup.py develop --cmake-only

# If you get error msgs, fix the error and rebuild
# less build/CMakeFiles/CMakeError.log
# make clean**

You may check the CMake summary output for Caffe2. Ensure USE_CUDA = 0

-- ******** Summary ********
-- General:
--   CMake version         : 3.20.2
--   CMake command         : /usr/bin/cmake
--   System                : Linux
--   C++ compiler          : /usr/bin/c++
--   C++ compiler id       : GNU
--   C++ compiler version  : 10.2.1
--   CXX flags             :  -
...
**--   USE_CUDA              : 0**

Also, you can use ccmake build to adjust build settings.

Finally, build and wait for hours. Take coffee and rest~

sudo python setup.py develop && python -c "import torch"

4.4 Compile PyTorch secure operators

source ${SGXSDK_ROOT}/environment
cd ${PyTorch_ROOT}/enclave_ops/secure_op && mkdir build && cd build
cmake -DCMAKE_PREFIX_PATH="$(python -c'import torch.utils; print(torch.utils.cmake_prefix_path)')" ..
make

If you get Caffe2 CUDA error, you shall go back to 4.3, adjust pytorch compile settings using ccmake and recompile.

5. ResNet-based test case

cd ${PyTorch_ROOT}/enclave_ops/test
sudo python whole_resnet.py

How sgx-pytorch works?

Python interface -> Custom C++ Operators -> C++ Enclave

Python interface

sgx-pytorch/torch/utils/secure_mkldnn.py at sgx · intel/sgx-pytorch (github.com)

def forward(self, x):
      torch.ops.load_library(LIB_PATH)
      return torch.ops.my_ops.secure_linear(
          x,
          self.encrypted_weight,
          self.encrypted_bias)
      #x_mkldnn = x if x.is_mkldnn else x.to_mkldnn()
      #y_mkldnn = torch._C._nn.mkldnn_linear(x_mkldnn, self.weight, self.bias)
      #y = y_mkldnn if x.is_mkldnn else y_mkldnn.to_dense()
      #return y

Custom C++ Operators

sgx-pytorch/enclave_ops/secure_op/op.cpp at d1aaf72fd309d650f0d04a53cdd31efcfacc1546 · intel/sgx-pytorch (github.com)

Extending TorchScript with Custom C++ Operators — PyTorch Tutorials 2.0.1+cu117 documentation

Call C++ Enclave

sgx-pytorch/aten/src/ATen/native/mkldnn/Linear.cpp at d1aaf72fd309d650f0d04a53cdd31efcfacc1546 · intel/sgx-pytorch (github.com)

Native Only Substrate Without Wasm

Although Wasm execution is a main feature of Substrate, you may want to disable it in some use cases. In this article, I will show you how to build a native-only Substrate chain without wasm.

Warning: Think twice in your design!

This is not a recommended way to build a Substrate chain.

The Parity team is removing native execution: The road to the native runtime free world #62. We are in the opposite direction.

If you want to use some native libraries which do not support no-std inside runtime pallet, you should consider using:

  1. offchain workers
  2. runtime_interface

Both of them are in the outer node, not in runtime, so they can use std libraries and not limited by wasm.

Environment Setup

Environment: substrate-node-template | tag: polkadot-v0.9.40 | commit: 700c3a1

Suppose you want to import an std Rust library called rust-bert into your runtime pallet. rust-bert is a machine learning library including large language models (LLMs) such as GPT2.

First, download substrate-node-template

git clone https://github.com/substrate-developer-hub/substrate-node-template
cd substrate-node-template
git checkout polkadot-v0.9.40

Build

Add rust-bert as a dependency in pallets/template/Cargo.toml.

You also need to specify getrandom as a dependency. Otherwise it will throw an error error: the wasm32-unknown-unknown target is not supported by default, you may need to enable the "js" feature. For more information see: https://docs.rs/getrandom/#webassembly-support

In runtime pallets, all your dependencies must:

  1. Support no-std.
  2. std is not enabled by default. (that’s what default-features = false accomplishes)

Otherwise, you will get an error error[E0152]: found duplicate lang item panic_impl when building. The reason is that std is leaking into the runtime code.

You can check this stackoverflow question for more details.

In pallets/template/Cargo.toml:

[dependencies]
rust-bert = { version = "0.21.0", default-features = false, features = ["remote", "download-libtorch"] }
getrandom = { version = "0.2", default-features = false, features = ["js"] }

However, rust-bert do no support no-std. Even if you add default-feature = false in Cargo.toml, it will still throw an error error[E0152]: found duplicate lang item panic_impl when running cargo build.

To fix this error, you should skip building wasm code by adding env SKIP_WASM_BUILD=1.

https://github.com/paritytech/substrate/blob/master/utils/wasm-builder/README.md#environment-variables

SKIP_WASM_BUILD=1 cargo build

Run

By now, You should successfully build a native-only Substrate target without wasm.

But running the target binary is not easy.

--execution native specify the execution strategy to native-first.

./target/debug/node-template --dev --execution native
Error: Input("Development wasm not available")

Search Development wasm not available in the codebase, you will find that it is thrown by node/src/chain_spec.rs.

pub fn development_config() -> Result<ChainSpec, String> {
    let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?;
  // ...
}

Since we don’t have wasm, we should remove this check.

let wasm_binary = WASM_BINARY.unwrap_or(&[] as &[u8]);

Rebuild and run again, new error occurs:

./target/debug/node-template --dev --execution native
2023-08-31 18:10:07 Substrate Node    
2023-08-31 18:10:07 ✌️  version 4.0.0-dev-700c3a186e5    
2023-08-31 18:10:07 ❤️  by Substrate DevHub <https://github.com/substrate-developer-hub>, 2017-2023    
2023-08-31 18:10:07 📋 Chain specification: Development    
2023-08-31 18:10:07 🏷  Node name: evanescent-agreement-4299    
2023-08-31 18:10:07 👤 Role: AUTHORITY    
2023-08-31 18:10:07 💾 Database: RocksDb at /tmp/substrate8yJbyt/chains/dev/db/full    
2023-08-31 18:10:07 ⛓  Native runtime: node-template-100 (node-template-1.tx1.au1)    
Error: Service(Client(VersionInvalid("cannot deserialize module: HeapOther(\"I/O Error: UnexpectedEof\")")))
2023-08-31 18:10:08 Cannot create a runtime error=Other("cannot deserialize module: HeapOther(\"I/O Error: UnexpectedEof\")")

This error is much harder to debug. I spend a lot of time to find the root cause.

Debug Process

Search online only finds one similar issue: https://github.com/paritytech/substrate/issues/7675. The core developer @bkchr suggests to create a dummy wasm binary to bypass the check.

I cannot find relevent information about constructing a dummy wasm binary. So I decide to debug the code step by step. Finally, I find the root cause is in one system dependency native_executor.rs.

There are 2 executors implementations: WasmExecutor and NativeElseWasmExecutor. When --execution native is specified, NativeElseWasmExecutor will be used.

NativeElseWasmExecutor wrap WasmExecutor as its field.

/// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence
/// and dispatch to native code when possible, falling back on `WasmExecutor` when not.
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
    /// Native runtime version info.
    native_version: NativeVersion,
    /// Fallback wasm executor.
    wasm:
        WasmExecutor<ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>>, 
}

During substrate node running, even if NativeElseWasmExecutor is used, it will still try to load wasm binary in 2 methods: runtime_version and call.

Let’s look at runtime_version at first:

impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D> {
    fn runtime_version(
        &self,
        ext: &mut dyn Externalities,
        runtime_code: &RuntimeCode,
    ) -> Result<RuntimeVersion> {
    Ok(self.native_version.runtime_version.clone()) // <--- Edit: We should return native version 

        self.wasm.runtime_version(ext, runtime_code) // <--- Original: it will try to load wasm binary
    }
}

Then, let’s look at call.

It will first check if the native version is compatible with the on-chain version. If it is compatible, it will call native executor. Otherwise, it will call wasm executor.

However, we don’t have valid wasm binary, so we should always call native executor. I just use the code inside this branch if use_native && can_call_with {}.

impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecutor<D> {
    type Error = Error;

    fn call(
        &self,
        ext: &mut dyn Externalities,
        runtime_code: &RuntimeCode,
        method: &str,
        data: &[u8],
        use_native: bool,
        context: CallContext,
    ) -> (Result<Vec<u8>>, bool) {
        // Edit
        // Do not check wasm since it is dummy, use native execution directly
        let used_native = true;
        let mut ext = AssertUnwindSafe(ext);
        let result = match with_externalities_safe(&mut **ext, move || D::dispatch(method, data)) {
            Ok(Some(value)) => Ok(value),
            Ok(None) => Err(Error::MethodNotFound(method.to_owned())),
            Err(err) => Err(err),
        };
    (result, used_native)
    
        // Original
        tracing::trace!(
            target: "executor",
            function = %method,
            "Executing function",
        );

        let on_chain_heap_alloc_strategy = runtime_code
            .heap_pages
            .map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
            .unwrap_or_else(|| self.wasm.default_onchain_heap_alloc_strategy);

        let heap_alloc_strategy = match context {
            CallContext::Offchain => self.wasm.default_offchain_heap_alloc_strategy,
            CallContext::Onchain => on_chain_heap_alloc_strategy,
        };

        let mut used_native = false;
        let result = self.wasm.with_instance(
            runtime_code,
            ext,
            heap_alloc_strategy,
            |_, mut instance, onchain_version, mut ext| {
                let onchain_version =
                    onchain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;

                let can_call_with =
                    onchain_version.can_call_with(&self.native_version.runtime_version);

                if use_native && can_call_with {
                      // call native executor
                    tracing::trace!(
                        target: "executor",
                        native = %self.native_version.runtime_version,
                        chain = %onchain_version,
                        "Request for native execution succeeded",
                    );

                    used_native = true;
                    Ok(with_externalities_safe(&mut **ext, move || D::dispatch(method, data))?
                        .ok_or_else(|| Error::MethodNotFound(method.to_owned())))
                } else {
                      // call wasm executor
                    if !can_call_with {
                        tracing::trace!(
                            target: "executor",
                            native = %self.native_version.runtime_version,
                            chain = %onchain_version,
                            "Request for native execution failed",
                        );
                    }

                    with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
                }
            },
        );
        (result, used_native)
    }

Finish!

You can use my substrate forked repo, which has already fixed the issue.

You should replace dependencies url in 3 Cargo.toml files:

git = "https://github.com/paritytech/substrate.git" -> git = "https://github.com/doutv/substrate.git"

replace

Build and run again, it should work now!

SKIP_WASM_BUILD=1 cargo build
./target/debug/node-template --dev --execution native

We successfully build a native-only Substrate chain without wasm!

Final code: substrate-node-template forked repo in native-only-polkadot-v0.9.40 branch, https://github.com/doutv/substrate-node-template/tree/native-only-polkadot-v0.9.40

Further Improvement

Modify NativeElseWasmExecutor is not the best solution, you may add an new executor implementation NativeOnlyExecutor.

Profiling ZKML Program in DIZK

Background

DIZK is a Java library for distributed zero knowledge proof systems, especially designed for Groth16.

My purpose is to run Circom program in DIZK, and test the performance improvement.

However, DIZK does not support parsing R1CS input.

Thanks for the Gnosis team, they forked DIZK repo and add R1CS input support in this PR https://github.com/gnosis/dizk/pull/8

It defines a DIZK R1CS JSON format: dizk/src/test/data/README.md at input_feed · gnosis/dizk · GitHub,

which is different from the .r1cs binary format defined by the Circom creator iden3 r1csfile/doc/r1cs_bin_format.md at master · iden3/r1csfile (github.com)

So, we should translate between these two R1CS formats.

Goal: Profiling Circom program in DIZK

In order to profile Cirom program in DIZK, we have to go through the following process:

Circom —Compiler—> R1CS .r1cs —We Try to Solve—> DIZK JSON ——> DIZK

Break it into 4 steps:

  • Step 1: Compile Circom program to R1CS and get witness data.
  • Step 2: Translate R1CS .r1cs and witness .wtns to DIZK JSON format.
  • Step 3: Feed the JSON to DIZK and test.
  • Step 4: Profile in DIZK with docker-compose.

The Circom program used today is zk-mnist.

https://github.com/0xZKML/zk-mnist

We will walk through the whole process step by step.

If you are only interested in profiling zk-mnist in DIZK, you may jump to my DIZK fork repo, and run profiling scripts. doutv/dizk: Java library for distributed zero knowledge proof systems (github.com)

Short Answer

I write a Python script to translate R1CS .r1cs and witness .wtns to DIZK JSON format.

First, compile Circom program to R1CS.

Second, export R1CS and witness to JSON.

name="circuit"

# Compile Circom
circom ${name}.circom --r1cs --wasm -o

# Generate Witness
node ./${name}_js/generate_witness.js ./${name}_js/${name}.wasm ${name}.input.json ${name}.wtns
snarkjs wtns check ${name}.r1cs ${name}.wtns

# Export r1cs to JSON
snarkjs r1cs export json ${name}.r1cs ${name}.r1cs.json -v

# Export witness to JSON
snarkjs wtns export json ${name}.wtns ${name}.wtns.json -v

Third, run this Python script and get DIZK JSON .dizk.json

Lastly, place .dizk.json under /src/test/data/json and add a testcase under class DistributedFromJSONTest

https://gist.github.com/doutv/d3ca6fa3a740c39266cd25c3480f681f

Long Answer

Step 1: Compile Circom program to R1CS and get witness data.

zk-mnist is a demo of ML for MNIST classification in a zero knowledge proof.

Clone the repo, compile the Circom code, and then we get .r1cs file.

git clone https://github.com/0xZKML/zk-mnist.git
yarn
yarn zk:ptau
yarn zk:compile

We can skip yarn zk:ptau because we are not going to use the ZK prove system here.

Let’s investigate what yarn zk:compile does.

It runs compile.sh zk-mnist/zk/compile.sh at main · 0xZKML/zk-mnist · GitHub

This script is well-documented, and it tells the way to create .r1cs file and generate witness.

# Compile the circuit. Creates the files:
# - circuit.r1cs: the r1cs constraint system of the circuit in binary format
# - circuit_js folder: wasm and witness tools
# - circuit.sym: a symbols file required for debugging and printing the constraint system in an annotated mode
circom circuit.circom --r1cs --wasm  --sym --output ./build

# Optional - export the r1cs
# yarn snarkjs r1cs export json ./zk/circuit.r1cs ./zk/circuit.r1cs.json && cat circuit.r1cs.json

# Generate witness
echo "generating witness"
node ./build/circuit_js/generate_witness.js ./build/circuit_js/circuit.wasm input.json ./build/witness.wtns

Also, to run this script, you should first install Circom compiler, and it requires Rust.

Install docs: Installation - Circom 2 Documentation

Now, you’ve run the compile script, and successfully generate r1cs file circuit.r1cs and witness data ./build/witness.wtns

Step 1 is done!

Step 2: Translate R1CS and witness to DIZK JSON format.

circuit.r1cs and witness.wtns are in binary format, and they are not human-readable.

The R1CS format for DIZK is human-readable JSON dizk/src/test/data/README.md at input_feed · gnosis/dizk · GitHub

Are there any existing tools to translate between binary and human-readable JSON?

The answer is YES! Thus, we don’t spend hours to write a translation program.

The answer hides in the script compile.sh.

yarn snarkjs r1cs export json ./zk/circuit.r1cs ./zk/circuit.r1cs.json

We have a readable R1CS JSON:

{
 "n8": 32,
 "prime": "21888242871839275222246405745257275088548364400416034343698204186575808495617",
 "nVars": 6417,
 "nOutputs": 16,
 "nPubInputs": 0,
 "nPrvInputs": 1344,
 "nLabels": 23489,
 "nConstraints": 5232,
 "constraints": [
  [
   {
    "0": "21888242871839275222246405745257275088548364400416034343698204186575808495615",
    "1339": "1"
   },
   {
    "0": "1",
    "1435": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
   },
     // ...
    ]
    "map": [...]
}

DIZK R1CS example:

{
  "header": ["2", "8", "3"], 
    // 2 is the length of primary_input
    // 8 is the length of aux_input
    // 3 is the length of constraints
  "primary_input":["1", "3"],
  "aux_input":["1", "1", "1", "1", "1", "1", "1", "2"],
  "constraints":[[{"2":"1"},{"5":"1"},{"0":"0","8":"1"}],
    [{"3":"1"},{"6":"1"},{"8":"-1","9":"1"}],
    [{"4":"1"},{"7":"1"},{"9":"-1","1":"1"}]
  ]
}

And you can notice that the constraints part matches perfectly, and nConstraints is the length of constraints.

For now, we have solved the constraints part. What about primary_input and aux_input?

They are stored in witness.wtns and their total length is nVars

How to translate witness binary format .wtns to JSON?

This is not easy, since compile.sh does not provide any tips.

I search wtns in snarkjs repo Code search results · GitHub, and find snarkjs/src/wtns_export_json.js surprisingly. The name already tells us it can export wtns to JSON.

Thanks to GitHub, I find a reference to this function snarkjs/cli.js at master · iden3/snarkjs · GitHub

snarkjs

It defines a command which can export witness to JSON.

# wtns export json [witness.wtns] [witnes.json]
snarkjs wtns export json ./build/witness.wtns ./witness.json

Run this command, and you get a witness JSON file.

[
    "1",
    "2",
    "2",
    "2",
        ...
]

It is an array with 6417 elements, which matches "nVars": 6417 in circuit.r1cs.json

How to split this array into 2 arrays primary_input and aux_input?

Witness data is a vector $z=[1 \ | \ public \ | \ private]$

primary_input stores 1 and public input. $[1 \ | \ public]$

aux_input stores the private input. $[private]$

For out example zk-mnist, it’s easy to find there is no public input "nPubInputs": 0

Thus, primary_input contains the first array element 1 , and the aux_input contains the rest.

"primary_input": ["1"],
"aux_input": ["2", "2", "2", ...]

Combine constraints part, and add header (length of each part), we get the final DIZK JSON input.

{
  "header": [
    "1",
    "6416",
    "5232"
  ],
  "primary_input": ["1"],
  "aux_input": [
    "2",
    "2",
    "2",
        ...
    ],
    "constraints": [
  [
   {
    "0": "21888242871839275222246405745257275088548364400416034343698204186575808495615",
    "1339": "1"
   },
   {
    "0": "1",
    "1435": "21888242871839275222246405745257275088548364400416034343698204186575808495616"
   },
     ...
    ]
}

You can check the complete JSON here: https://github.com/doutv/dizk/blob/aea40e532bd1cb0e44ffeabaaaaa7ecc92f7b813/src/test/data/json/zkmnist.json

Step 3: Feed the data to DIZK and test

Congrats! This is the easiest step.

Gnosis team already write tests for JSON input. Just follow them and append testcases.

We can test both in serial and distributed way:

  1. Place the Step 2 result in src/test/data/json/zkmnist.json

  2. Add testcases in DistributedFromJSONTest and SerialFromJSONTest

    1. https://github.com/doutv/dizk/blob/aea40e532bd1cb0e44ffeabaaaaa7ecc92f7b813/src/test/java/input_feed/DistributedFromJSONTest.java#L78
    2. https://github.com/doutv/dizk/blob/aea40e532bd1cb0e44ffeabaaaaa7ecc92f7b813/src/test/java/input_feed/SerialFromJSONTest.java#L75
    @Test
    public void distributedR1CSFromJSONTest3() {
        String filePath = "src/test/data/json/zkmnist.json";
        converter = new JSONToDistributedR1CS<>(filePath, fieldFactory);
    
        r1cs = converter.loadR1CS(config);
        assertTrue(r1cs.isValid());
    
        witness = converter.loadWitness(config);
        assertTrue(r1cs.isSatisfied(witness._1(), witness._2()));
    }
    
  3. mvn -Dtest=DistributedFromJSONTest test

  4. mvn -Dtest=SerialFromJSONTest test

If test failed, that means the JSON input is wrong, and it is not a valid R1CS.

You should go back to Step 2 and check the differences.

Step 4: Profile in DIZK with Docker Compose

See my DIZK fork repo: doutv/dizk: Java library for distributed zero knowledge proof systems (github.com)

My environment: Windows 11 + WSL2 Ubuntu 22.04

Since profiling in real Spark cluster requires tedious environment settings, we can setup Spark cluster in docker-compose with single machine.

Setup Spark Cluster in Docker Compose

I publish a Spark Cluster Docker Image on Docker Hub, so that you can skip building image from scratch. backdoorabc/cluster-apache-spark:2.1.0

dizk/docker-compose.yml at master · doutv/dizk (github.com)

docker-compose up -d OR docker compose up -d

Running profiler script dizk/src/main/java/profiler/scripts/docker_zkmnist.sh at master · doutv/dizk (github.com) would produce many .csv files under /out folder.

  • Output csv format

    # All times are in seconds
    # The lasest output will be appended to the last row of the csv
    
    Load R1CS-1exec-1cpu-1mem-1partitions.csv
    Load R1CS time
    
    Load Witness-1exec-1cpu-1mem-1partitions.csv
    Load Witness time
    
    Setup-1exec-1cpu-1mem-1partitions.csv
    Constraint Size, Total Setup time, Generating R1CS proving key, Computing gammaABC for R1CS verification key
    
    Prover-1exec-1cpu-1mem-1partitions.csv
    Constraint Size, Total Prover time, ?
    
    Verifier-1exec-1cpu-1mem-1partitions.csv
    Constraint Size, Total Verifier time, pass(1) or fail(0)
    

To analyse the performance improvement of distributed computing, I create a jupyter notebook to visualize results. dizk/data-analysis.ipynb at master · doutv/dizk (github.com)

This is my profiling result.

output

三年总结

回顾过去三年在 CS 领域走过的路

许多具体的学习细节现在都已经忘记了,通过 GitHub Commit 记录和博客来回忆。

2019.9 - 2019.12

  • 大一上学期学 CS 的时间并不多,更多时间花在旅游和学习必修课上了,现在看来挺幸运的,赶在疫情前去了好几个城市
  • 加入了学长的创业公司——北极星工作室,学了点 Node.js 并给一个后端项目重构代码,第一次体验软件开发
  • 还参加了机器人社团,学了点 Python 和 Arduino,体验了简单的硬件开发
  • 用 AWS 的学生优惠体验了 Linux 服务器
  • 当时自己没有特别感兴趣的方向,每个方向都探索了一下下,也没有学习压力,一段轻松且美好的时光

2020.1 - 2020.8

  • 疫情爆发,在家学习(打游戏),这段时间学习和打游戏的效率都蛮高的
  • 学了一周 Python 爬虫:https://segmentfault.com/blog/papapa
  • The Missing Semester of Your CS Education by MIT 学习了这门很棒的课程,教各种基础工具和概念
  • 玩了几个月的算法竞赛(高中有一点算法竞赛的基础),Leetcode 和 Google Kick Start,现在看当时写的题解,很多题目都不会做了…
  • 华为软件精英挑战赛,跟同学一起进了复赛,拿了二等奖,这是目前唯一能写在简历上面的奖项
  • 机器学习初体验
  • 上了 CSC3170 数据库,认识了几个很厉害的学长学姐,学了些用处不大的数据库设计理论
  • 跟两位前端同学一块做了个校内问答网站,我用刚学到的数据库知识和现学的 Django 写后端。后来跟他们一起在校内创业,我也由此进入了 Python 后端开发的深坑。
  • 大一结束后,我意识到学校的 CS 教育不适合我,进度太慢,跟不上时代,唯一的学习动力只是 GPA。我决定把学习重点放在课外,在课外找到自己的兴趣点并为之努力,课内就随便水水。

2020.9 - 2020.12

  • 面试字节后端实习岗,被问了一道 Leetcode Hard 题目然后挂了:第一次技术面试
  • 上了 20 学分的课:GFH + ENG + 物理实验 +离散数学 + Cpp + 操作系统 + 数字电路
  • 跟小伙伴一起校内创业,一家名为 TeaBreak 的公司
    • 继续做 Python 后端开发,还做一些运维的工作,Django + Nginx + MySQL + DNS + CDN
    • 参与了第一次招新,第一次当面试官,认识了很多新朋友

2021.1 - 2021.5

  • 这学期只上了 10 学分的课,主要在创业公司做 Python 后端开发
  • 现在看来,当时做的事情也没啥技术难度,实现细粒度的权限管理
  • 培养了用 Google 和 Stack Overflow 解决 bug 以及看英文文档的能力

2021.6 - 2021.12

  • 字节二面挂了,简历上没啥亮点项目,算法题不够熟练,基础知识也不扎实,能进大厂就怪了
    • 现在想来,没去也好,要去北京实习租房,而且还是教育线的,要是去了的话,估计没到三个月就被裁了😂
  • 凭借东拼西凑的工程能力,我捡漏进了一家小厂
  • 在晶泰科技实习了半年,9 月到 12 月边上课边实习,一周去 3 天
  • 详见:https://huangyongjin.com/2021/11/26/2021/xtalpi-intern-experience/
    • 前四个月还是做 Python 后端开发,只不过是从我熟悉的 Django + MySQL 换成了 Flask + Neo4j
    • 后两个月做 Go 后端开发,从零开始学,在项目中学习的效率很高
    • 期间还学习和体验了敏捷开发和 DevOps,深入学习了 Docker
  • 这段经历还是十分宝贵的,让我同时体验了业务开发和基础架构开发两个方向
    • Python 后端开发没有前途,市面上岗位很少,只适合做小型应用
    • 个人感觉业务开发挺无聊的,基本是 CURD,需求还会经常变,技术难度低
    • 基础架构开发似乎有意思些,需求变化较慢,技术难度高
    • 当然,业务开发也没有这么糟糕,它离客户更近,可以培养产品能力
    • 可以看下这篇文章:https://mp.weixin.qq.com/s/1GhZq-jOPrHCT-alAcKHJg
  • 因为大二下暑课生物拿了 C-,所以这学期要认真卷,GPA 才能过 3.3,最后出人意料地拿了 4.0 😝

2022.1 - 2022.5

  • 前三个月,疫情爆发,在家摆烂
  • 学了计算机图形学,暂时也用不上,领悟到了做渲染器很难
  • 四月回校后开发 TCP 教学游戏,学了一些 TCP 和前端知识

2022.6 - 2022.7

  • 本来打算暑假出去玩一两个月,然后准备秋招的
  • 突然看到一个校内 web3 实习的机会,不用通勤,工资不错,还可以接触新领域,于是就面试并成功入职了
  • 接触到 web3 后,觉得这个领域前景广阔,萌生了 all in web3 的想法
  • 学习 Rust,深入了解波卡(Polkadot)生态
  • 初学 Haskell 和范畴论,数学也蛮有意思的哎

总结

  • 大一:探索兴趣,寻找方向
  • 大二:后端开发和运维入门
  • 大三:深入底层,思考方向
  • 现在想起来,其实有点后悔自己没有早点学底层知识,太早去做应用层开发了
  • 当然,现在学底层知识也不晚

跨链桥概述

好文必读:https://medium.com/1kxnetwork/blockchain-bridges-5db6afac44f8

功能范围

  • Asset-specific 特定资产
  • Chain-specific 特定的两条链之间
  • Application-specific 特定应用
  • Generalized 通用跨链协议

功能范围

评价维度

  • Security 安全性: Trust & liveness assumptions, tolerance for malicious actors, the safety of user funds, and reflexivity.
  • Speed 速度: Latency to complete a transaction, as well as finality guarantees. There is often a tradeoff between speed and security. 速度与安全性通常不可兼得
  • Connectivity 连通性: Selections of destination chains for both users and developers, as well as different levels of difficulty for integrating an additional destination chain. 是否容易添加目标链
  • Capital efficiency 经济性: Economics around capital required to secure the system and transaction costs to transfer assets. 保障系统安全的资本和交易费用
  • Statefulness 状态性: Ability to transfer specific assets, more complex state, and /or execute cross-chain contract calls. 传输特定资产,复杂状态,跨链执行合约
    设计取舍

评价维度

安全性保证
Security 又可细分为以下维度:

  • Trust-less 无信任:享有与其所连接的区块链的安全性
  • Insured 保险:桥梁运营者需要质押,Insured 比 Bonded 更好,如果桥梁运营者作恶,会直接扣除其质押品并补偿给用户
  • Bonded 债券:桥梁运营者需要质押
  • Trusted 需要信任:只依赖于桥梁运营者的信誉,没有抵押品

安全性保证

验证机制

  • External validators & Federations 外部公证人
    • 在多个公证人之间达成共识
    • statefulness and connectivity 方面很好
    • 但是 security 较差,用户依赖于公证人的安全性,而公证人大多数是 trusted 的模型,即使是使用质押模型也存在一些问题
  • Light clients & Relays 轻客户端
    • 源链产生对交易的证明,发给目标链的合约A,合约A验证交易并执行
    • 优点:
      • trustless 安全性强
      • Statefulness 状态性强:可以传输任何类型的数据
      • capital-efficient:不需要质押资金
    • 缺点:
      • 消耗资源多 源链和目标链都要部署智能合约,且验证交易需要gas费
      • connectivity 连通性差 :对于每对区块链,都需要分别部署两个智能合约
      • Speed 速度慢
  • Liquidity networks 流动性网络
    • 中间节点同时持有源链和目标链的资产
    • 优点:
      • Security 安全性强
      • Speed 速度快
      • capital efficient than bonded/insured external validators 比质押的外部公证人模型要更经济,因为资金只是为了保障流动性而不是安全性
    • 缺点:
      • statefulness 状态性差:不能传输任意消息

验证机制

Substrate 学习

Substrate的文档高速迭代,链接可能失效,参见 https://docs.substrate.io/

使用方法

  1. 使用substrate node: In this case, you just need to supply a JSON file and launch your own blockchain. The JSON file allows you to configure the genesis state of the modules that compose the Substrate Node’s runtime, such as: Balances, Staking, and Sudo.

  2. 使用 substrate frame,frame 里有很多 pallet 可以自由选择 This affords you a large amount of freedom over your blockchain’s logic, and allows you to configure data types, select from a library of modules (called “pallets”), and even add your own custom pallets.

  3. 使用 substrate core,不使用 frame,用 wasm 来实现 the runtime can be designed and implemented from scratch. This could be done in any language that can target WebAssembly

Substrate 组件

Storage 底层存储数据库,用于持久保持底层区块链的不断变化的状态

Runtime 区块链的业务逻辑,需要达成链上共识

Peer-to-peer network P2P通信

Consensus 与其他节点达成共识

RPC Substrate 提供 HTTP 和 WebSocket 两种 RPC 接口

Telemetry 监控

搭建 PoA 私有网络

目标:创建一个自己的区块链网络,包括学会了生成自己的账号密钥、导入密钥到密钥库,并创建一个使用该密钥对的自定义创世区块配置文件(chain spec)

【Substrate 入门】创建我们自己的区块链网络 - 海阳之新的文章 - 知乎

https://zhuanlan.zhihu.com/p/340583890
PoA = Aura

The Substrate node template uses a proof of authority consensus model also referred to as authority round or Aura consensus. The Aura consensus protocol limits block production to a rotating list of authorized accounts—authorities—that create blocks in a round robin fashion.

设计哲学

Substrate 入门 - Substrate 的模型设计 -(七) - 金晓的文章 - 知乎

https://zhuanlan.zhihu.com/p/101058405

比较现代计算机的程序模型与当前的区块链模型

  1. 程序由指令与数据构成,对应到链上即为链上代码(例如以太坊的合约,fabric 的 chaincode,substrate 的 Runtime)和链上存储(以太坊和 fabric 都叫做世界状态 world state,Substrate 中叫 Runtime Storage,也是世界状态)
  2. 程序接受用户的输入,经过处理后得到输出,对应到链即为接受区块中的交易,执行后修改状态。用户可以以异步的方式去查询执行后的结果以代表执行后的输出。请注意区块由于需要经过共识的过程,因此对于结果的判定一定得等到区块的共识达成(又称为区块 finality 后),才能进行查询。因此区块链是一个异步的系统。这里接受交易的调用即是接受一个 Extrinsic,外部的的输入。
  3. 程序需要运行在计算机的操作系统环境里(非指代无需操作系统的程序),对应到链而言是运行链上代码所需要的一个沙盒环境,这个沙盒环境是要去除 io,网络访问等会产生“副作用”的沙盒。在以太坊中这个环境是 EVM,fabric 是 docker,substrate 中即是 Runtime 的运行环境(如 wasm 的沙盒环境)

区分 Runtime 和其他部分

Substrate 设计总览 - 金晓的文章 - 知乎

https://zhuanlan.zhihu.com/p/56383616
除了 Runtime 以外的其他部分提供了系统的基础功能,所有的链都应该具备这些基础功能,开发者通过修改 Runtime 定制链的逻辑

需要对运行结果进行共识的功能部分应该归属于 Runtime

这里有一个简单的判定标准判断某个功能是否应该放在 Runtime 内:

对于某个功能,若只改动一个节点的代码对于所有的逻辑运行的结果与其他不改动的节点运行的结果相同,则认为这个部分应该放在 Runtime 之外,如果运行结果不同,则认为应该放在 Runtime 之内。

举个例子:比如我改变了交易池的排序代码,使得对某个账户有利的交易能优先打包。这个改动会令自己这个节点产出的区块不公平的打包交易,但是只要打包出来的区块大家都可以认可,则所有节点共识的“状态的变化”仍然是一致的。很明显,这个功能组件不应该是 Runtime 的功能,因为它不会改变对于验证一个区块时的“状态变化”的验证。比如我改变了转账功能的代码,能给某个账户凭空增加钱,那么显然,这种改动对于这个改动过的节点执行的结果将会与其他节点不同,则共识不会通过。所以转账这个功能就应该放在 Runtime 当中,让所有节点执行的都是一致的。

无分叉升级

Substrate 设计总览 - 金晓的文章 - 知乎

https://zhuanlan.zhihu.com/p/56383616

Runtime 在 Substrate 框架下,将会用同一份代码编译出两份可执行文件:

  • 一份 Rust 的本地代码,我们一般称为 native 代码,native 与其他代码无异,是这个执行文件中的二进制数据,直接运行。在 Substrate 的相关代码以 native 命名
  • 一份 wasm 的链上代码,我们一般称为 wasm 代码,wasm 被部署到链上,所有人可获取,wasm 通过构建一个 wasm 的运行时环境执行 。在 Substrate 的相关代码以 wasm 命名
    由于这两份代码是由相同的代码编译出来的,所以其执行逻辑完全相同 (有一些很小的暗坑要注意)。其中 wasm 将会部署到链上,所有人都可以获取到,也就是说即使本地运行的不是最新版本的节点,只要同步了区块,一定可以获取到最新的 wasm 代码。

换句话说,一个写在 Runtime 内部的代码,也就是代表这条链功能性的代码,存在两份,分别是 native 与 wasm。wasm 代码被部署到链上,是“链上数据”,可以通过同步区块所有人统一获取并执行。这样就可以保证在区块链中所有矿工执行的都是最新的代码。
P.S. 这里需要强调,代码的部署可以通过“民主提议”,“sudo 控制权限”,“开发者自定一种部署条件”等方式进行,到底哪种方式“更区块链”,“更合理”,不在本文讨论范围内,这与这条链的设计目的相关。Substrate 只是提供了这种“热更新”的强大机制,如何使用这种机制是这条链的问题。

native 和 wasm 的执行策略

以下策略均满足:只要 wasm 的版本比 native 高,就一定执行 wasm

  • NativeWhenPossible: Execute with native build (if available, WebAssembly otherwise).
  • AlwaysWasm: Only execute with the WebAssembly build.
  • Both: Execute with both native (where available) and WebAssembly builds.
  • NativeElseWasm: Execute with the native build if possible; if it fails, then execute with WebAssembly.

不同的部分可以有不同的执行策略:

The default execution strategies for the different parts of the blockchain execution process are:

  • Syncing: NativeElseWasm
  • Block Import (for non-validator): NativeElseWasm
  • Block Import (for validator): AlwaysWasm
  • Block Construction: AlwaysWasm
  • Off-Chain Worker: NativeWhenPossible
  • Other: NativeWhenPossible

总结:

  • native 会比 wasm 运行速度更快
  • wasm 能得到各节点的共识,更应该使用 wasm 来做区块生成

核心概念

DApp 是什么:DApp 是可以自主运行的应用程序,通常通过使用智能合约,在去中心化计算的区块链系统上运行。

Pallet 是什么:pallet 就是定义你的区块链能做什么的单个逻辑片段(比如 babe,grandpa,evm,contracts 都是 substrate 预先建立的 pallet)

Frame:a set of modules (pallets) and support libraries that simplify runtime development. 帮助开发 runtime

runtime:应用逻辑,It defines what transactions are valid and invalid and determines how the chain’s state changes in response to transactions. Everything other than the runtime, does not compile to Wasm, only to native. The outer node is responsible for handling peer discovery, transaction pooling, block and transaction gossiping, consensus, and answering RPC calls from the outside world.(它定义了哪些交易是有效的,哪些是无效的,并决定了链的状态如何响应交易而变化。”外部节点”,除运行时之外的一切,不编译为 Wasm,只编译为本地的。外层节点负责处理对等的发现,交易池,区块和交易的八卦,共识,以及回答来自外部世界的 RPC 调用。)

Aura

Authority round, 通过一个 authorities 列表,轮流发布区块,能够长时间在线的 authorities 就是 honest authorities。

Extrinsics 输入

在 Substrate 中的交易不再称为 Transaction,而是称为了 Extrinsic,中文翻译就是“外部的;外表的;外源性”,意味着被称为 Extrinsic 的概念,对于区块链而言是外部的输入。这种定义脱离了本身“交易”的范畴(更像是转账的概念),而是在链的状态的角度下,认为交易及类似概念是一种改变状态的外部输入(意味着不止转账,只要是外部的操作都是)。

Substrate 入门 - 交易体 -(六) - 金晓的文章 - 知乎

https://zhuanlan.zhihu.com/p/100770550

作者:金晓
链接:https://zhuanlan.zhihu.com/p/100770550
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
An extrinsic is a piece of information that comes from outside the chain and is included in a block.

A block in Substrate is composed of a header and an array of extrinsics.

三种类型:

  • Inherents: pieces of information that are not signed and only inserted into a block by the block author. 不需要签名,是由足够多 validator 同意认为是正确的东西。inherent 是 substrate 真正区别于其他区块链,也能够成为 blockchain as world computer 的原因。
  • Signed transactions: contain a signature of the account that issued the transaction and stands to pay a fee to have the transaction included on chain.
  • Unsigned transactions: Since the transaction is not signed, there is nobody to pay a fee.

Transaction lifecycle

Block produced by our node

  1. Our node listens for transactions on the network.
  2. Each transaction is **verified **and valid transactions are placed in the transaction pool.
  3. The pool is responsible for **ordering **the transactions and returning ones that are ready to be included in the block. Transactions in the ready queue are used to construct a block.
  4. Transactions are executed and state changes are stored in local memory. Transactions from the ready queue are also propagated (gossiped) to peers over the network. We use the exact ordering as the pending block since transactions in the front of the queue have a higher priority and are more likely to be successfully executed in the next block.
  5. The constructed block is published to the network. All other nodes on the network receive and execute the block.

Block received from network

The block is executed and the entire block either succeeds or fails.

Transaction Weight

Weights are the mechanism used to manage the time it takes to validate a block.

Execution

Executive module: Unlike the other modules within FRAME, this is not a runtime module. Rather it is a normal Rust module that calls into the various runtime modules included in your blockchain.

Off-Chain Features

Compared to oracle
Off-chain features run in their own Wasm execution environment outside of the Substrate runtime. This separation of concerns makes sure that block production is not impacted by long-running off-chain tasks.

However, as the off-chain features are declared in the same code as the runtime, they can easily access on-chain state for their computations.

Finality

The part of consensus that makes the ongoing progress of the blockchain irreversible. After a block is finalized, all of the state changes it encapsulates are irreversible without a hard fork. The consensus algorithm must guarantee that finalized blocks never need reverting. However, different consensus algorithms can define different finalization methods.

  • deterministic finality, each block is guaranteed to be the canonical block for that chain when the block is included. Deterministic finality is desirable in situations where the full chain is not available, such as in the case of light clients. GRANDPA is the deterministic finality protocol that is used by the Polkadot Network.
  • probabilistic finality, finality is expressed in terms of a probability, denoted by p, that a proposed block, denoted by B, will remain in the canonical chain. As more blocks are produced on top of B, p approaches 1.
  • instant finality, finality is guaranteed immediately upon block production. This type of non-probabilistic consensus tends to use practical byzantine fault tolerance (pBFT) and have expensive communication requirements.

Runtime 开发

FRAME

Framework for Runtime Aggregation of Modularized Entities (FRAME) is a set of modules (pallets) and support libraries that simplify runtime development.

一个 pallet 的结构:

  1. Imports and Dependencies
  2. Declaration of the Pallet type
  3. Runtime Configuration Trait
  4. Runtime Storage
  5. Runtime Events
  6. Hooks
  7. Extrinsics

Macro

pallet attribute macro

// 1. Imports and Dependencies
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
    use frame_support::pallet_prelude::*;
    use frame_system::pallet_prelude::*;

    // 2. Declaration of the Pallet type
    // This is a placeholder to implement traits and methods.
    // https://paritytech.github.io/substrate/master/frame_support/attr.pallet.html#pallet-struct-placeholder-palletpallet-mandatory
    // To generate a `Store` trait associating all storages, use the attribute `#[pallet::generate_store($vis trait Store)]`
    #[pallet::pallet]
    #[pallet::generate_store(pub(super) trait Store)]
    pub struct Pallet<T>(_);

    // 3. Runtime Configuration Trait
    // All types and constants go here.
    // Use #[pallet::constant] and #[pallet::extra_constants]
    // to pass in values to metadata.
    #[pallet::config]
    pub trait Config: frame_system::Config { ... }

    // 4. Runtime Storage
    // Use to declare storage items.
    #[pallet::storage]
    #[pallet::getter(fn something)]
    pub type MyStorage<T: Config> = StorageValue<_, u32>;

    // 5. Runtime Events
    // Can stringify event types to metadata.
    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> { ... }

    // 6. Hooks
    // Define some logic that should be executed
    // regularly in some context, for e.g. on_initialize.
    // generate a default implementation
    #[pallet::hooks]
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { ... }

    // 7. Extrinsics
    // Functions that are callable from outside the runtime.
    #[pallet::call]
    impl<T:Config> Pallet<T> { ... }

}

现存的 pallet

Metadata

对于每个 pallet 提供存储项、外在调用、事件、常量和错误的信息。

元数据不变,所以旧区块上的元数据可能是过期的。只有在链的 runtime spec_version 变的时候,metadata 才会变。

rust 调用函数为 state_getMetadata

Storage

存储允许在区块链中存数据,这些数据持续存在,runtime logic 可以访问。

Storage 这个 module 是 substrate 的存储 Api,可以自选最适合的存储决定。

Storage Map

key-value 对实现,用来管理其元素会被随机访问而不是按顺序完整迭代的 items

Iterating over Storage Maps

可迭代,因为 map 经常用于跟踪无限制的数据,所以必须谨慎。

Origins

用来确认一个 call 的来源

三种类型

1:root

2:sighed

3:unsighed

Events

当一个托盘想把运行时的变化或条件通知外部实体,如用户、chain explorers 或 dApps 时,它可以使用事件。

可以定义事件的信息和这些事件何时被发出。

//用宏创建
#[pallet::event]

#[pallet::metadata(u32 = "Metadata")]
_pub_ _enum_ Event<T: Config> {
    /// Set a value.
    ValueSet(_u32_, T::AccountId),
}

在 pallet config 里去实现 event

#[pallet::config]
pub trait Config: frame_system::Config {
        /// The overarching event type.
        type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
    }
// runtime/src/lib.rs event事件需要对runtime暴露
impl template::Config for Runtime {
    type Event = Event;
}
// runtime/src/lib.rs

construct_runtime!(
    pub enum Runtime where//支持 Runtime generic type 需要使用where子句
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
    {
        // --snip--
        TemplateModule: template::{Pallet, Call, Storage, Event<T>},
        //--add-this------------------------------------->^^^^^^^^
    }
);

事件的存储(使用宏)

// 1. Use the `generate_deposit` attribute when declaring the Events enum.
#[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)] 
    //super和self都是rust里的关键词,一个指向parent scope(或是当前mod以外的scope),一个指向当前的scope
    #[pallet::metadata(...)]
    pub enum Event<T: Config> {
        // --snip--
    }

// 2. Use `deposit_event` inside the dispatchable function
#[pallet::call]
    impl<T: Config> Pallet<T> {
        #[pallet::weight(1_000)]
        pub(super) fn set_value(
            origin: OriginFor<T>,
            value: u64,
        ) -> DispatchResultWithPostInfo {
            let sender = ensure_signed(origin)?;
            // 默认调用deposit_event来将事件写入存储
            Self::deposit_event(RawEvent::ValueSet(value, sender));
        }
    }

每个 block 只存储这个区块开始之后发生的事件。

deposit_even 可改写。

事件支持的类型

可以接受所有支持 Parity SCALE 编解码器的类型,如果是 Runtime generic type 需要使用 where 子句来定义。(在上文 construct_runtime!里)

对事件的响应

不可以查询事件,但是可以看到 pallet 存储着的事件的列表信息。或者是使用前端借口可以对 event 进行订阅。

事件可能会出现的错误

鉴于 rust 的错误处理比较优雅,所以主要在返回结果类型里做特殊处理。

Err

DispatchResult:调用结果返回 DispatchError 是 DispatchResult 的一个变体,表示可调度函数遇到了错误

//重写自己pallet特别的DispatchError by macro

#[pallet::error]

pub enum Error<T> {
        /// Error names should be descriptive.
        InvalidParameter,
        /// Errors should have helpful documentation associated with them.
        OutOfSpace,
    }

可以使用 ensure!宏来检查预设条件,不满足预设条件也会返回 error

例子:Events and Errors by completing the Substrate Kitties tutorial

Weights and Fees

pallet-transaction-payment

Substrate 区块链应用的交易费用设计

收费目的:

1:激励服务提供方即开发团队和节点

2:调节资源利用率

计算费用的方式

参数(base fee, weight fee, length fee, tip)

base fee:一笔交易的最低金额

weight fee:与交易消耗的执行时间成比例。在有限的区块生成时间和链上状态的限制下,weight 被用来定义交易产生的计算复杂度即所消耗的计算资源,以及占据的链上状态。

length fee:编码长度有关

tip:可选小费,优先级

使用 transaction-payment pallet 来实现

提供的是基础的收费逻辑

提供一些配置的特征(比如 Config::WeightToFee 将一个 weight 值转换成基于货币类型的可扣除费用;Config::FeeMultiplierUpdate,根据上一个区块结束时链的最终状态,通过定义一个乘数来更新下一个区块的费用;使用 Config::OnChargeTransaction 管理交易费用的提取、退款和存款。)

inclusion_fee = base_fee + length_fee + [targeted_fee_adjustment * weight_fee];

final_fee = inclusion_fee + tip;

余额不足的账户一般不会执行交易,因为有 transaction queue 和 block-making logic 去执行检查。

提供 FeeMultiplierUpdate 这种参数让使用者可以配置,去实现对 weight 的调节(如果这个区块比较饱和,那 weight 可以提高)

有特殊要求的交易

比如质押金,比如存款

默认的权重注释

substrate 里的可调度函数必须指定一个 weight,注释帮助我们确定 weight 值的设定,也可以帮助我们更有效地优化代码

weight 相关的注释应当要包含对 runtime 方法的执行成本有明显影响的部分。比如:存储相关的操作 (read, write, mutate, etc.)|Codec(Encode/Decode)相关操作(序列化/反序列化 vecs 或者大的结构体)|search/sort 等成本高的计算 | 调用其他 pallet 中的方法

#[pallet::weight(100_000)]
fn my_dispatchable() {
    // ...
}
//ExtrinsicBaseWeight会自动添加到声明的这个weight里,方便考虑到简单将一个空的外在因素纳入一个块中的成本

//

#[pallet::weight(T::DbWeight::get().reads_writes(1, 2) + 20_000)]

fn my_dispatchable() {
    // ...
}
  • 同一个值的多次读取算作一次读取。

  • 同一数值的多次写入算作一次写入。

  • 多次读同一个值,然后写这个值,算作一次读和一次写。

  • 写入后的读只算作一次写入。

调用类 Dispatch Class

Normal(正常用户能触发的 transaction,消耗一个区块的一部分 total weight limit,可以用 AvailableBlockRatio 去找到), Operational(由网络中的管理员或者管理委员会共同触发,提供网络能力的调度,这些类型的 dispatch 会消耗一整个区块的全部 weight 限制,不受 AvailableBlockRatio 的约束,优先权最大,并且不需要支付 tip), and Mandatory(被包含在一个块中, 即使它超过了 total weight limit。 应用于 inherents, 代表作为块验证过程一部分的功能)

//默认DispatchClass是normal

//最后的参数是决定是否根据注释的权重向用户收费,默认为yes

#[pallet::weight((100_000, DispatchClass::Operational), Pays::No)]

fn my_dispatchable() {
    // ...
}

动态 weights

使用 Substrate 预定义的 FunctionOf 结构体, 增加对函数的输入的考虑。FunctionOf 接收三个数据,a) 一个根据参数计算权重的 closure 表达式; b) 固定交易级别或计算交易级别的 closure; c) 设置 pays_fee 的布尔值。

#[pallet::weight(FunctionOf(
  |args: (&Vec<User>,)| args.0.len().saturating_mul(10_000),
  DispatchClass::Normal,
  Pays::Yes,
))]
fn handle_users(origin, calls: Vec<User>) {
    // Do something per user
}

派送后的重量校正

可能消耗重量比之前规定的要少,为了修正,返回一个不一样的返回类型

#[pallet::weight(10_000 + 500_000_000)]

fn expensive_or_cheap(input: u64) -> DispatchResultWithPostInfo {
    let was_heavy = do_calculation(input);

    if (was_heavy) {
        // None means "no correction" from the weight annotation.
        Ok(None.into())
    } else {
        // Return the actual weight consumed.
        Ok(Some(10_000).into())
    }
}

自定义的费用

可以自己实现。但是必须要满足以下 trait

  • [WeighData<T>]: To determine the weight of the dispatch.
  • [ClassifyDispatch<T>]: To determine the class of the dispatch.
  • [PaysFee<T>]: To determine whether the dispatchable’s sender pays fees.
    内附代码实现,需要自定义的时候可以参考。

Benchmarking

因为 substrate 要求以固定的目标区块时间来生产快,所以区块链只能在每个区块里执行有限数量的 extrinsics。执行一个 extrinsic 的时间会根据计算复杂性、存储复杂性、使用的硬件和许多其他因素而变化。

substrate 不使用 gas metering 来计算 extrinsic 是因为这引入了大量的额外开销,使用 benchmark 提供一个在最坏情况下的最大近似值。先扣费,之后可以退还。

Denial-of-service (DoS) 是分布式系统(包括区块链网络)的一个常见攻击媒介。也就是说用户可以反复执行需要大量算力的 extrinsic,所以系统必须收费才能让用户不滥用网络。benchmark 作为一个 pallet 的功能是尽可能估计准确的成本,通过在模拟运行时环境中多次执行一个托盘的外部因素,并跟踪执行时间来实现。

benchmarks! {

  benchmark_name {
    /* set-up initial state */
  }: {
    /* the code to be benchmarked */
  } verify {
    /* verifying final state */
  }
}
//定义初始状态,测量执行时间,以及数据库的读写次数。最后验证运行时间的最终状态验证是否完成了预期。

Debugging

1:日志,有 debug 和 info 的宏来帮助完成

  log::info!("called by {:?}", who);

2:Printable trait, 用它来打印(u8、u32、u64、usize、&[u8]、&str 已实现,自定义类型可自行补充)

use sp_runtime::traits::Printable;

use sp_runtime::print;
#[frame_support::pallet]

pub mod pallet {
    // The pallet's errors
    #[pallet::error]
    pub enum Error<T> {
        /// Value was None
        NoneValue,
        /// Value reached maximum and cannot be incremented further
        StorageOverflow,
    }

    impl<T: Config> Printable for Error<T> {
        fn print(&self) {
            match self {
                Error::NoneValue => "Invalid Value".print(),
                Error::StorageOverflow => "Value Exceeded and Overflowed".print(),
                _ => "Invalid Error Case".print(),
            }
        }
    }
}

3:Substrate’s 自带的 print 函数

_use_ sp_runtime::print;

_pub_ _fn_ do_something(origin) -> DispatchResult {

    print!("Execute do_something");
    }

4;if std

当你不仅仅想打印,也不想用什么 trait 的时候可以用 if_std!

//使用if_std!,但是只有运行native的时候才会执行
use sp_std::if_std; // Import into scope the if_std! macro.

#[pallet::call]

impl<T: Config<I>, I: 'static> Pallet<T, I> {
        // --snip--
        pub fn do_something(origin) -> DispatchResult {

            let who = ensure_signed(origin)?;
            let my_val: u32 = 777;

            Something::put(my_val);

            if_std! {
                // This code is only being compiled and executed when the `std` feature is enabled.
                println!("Hello native world!");
                println!("My value is: {:#?}", my_val);
                println!("The caller account is: {:#?}", who);
            }

            Self::deposit_event(RawEvent::SomethingStored(my_val, who));
            Ok(())
        }
        // --snip--
}

Testing

1:单元测试,使用现成 cargo 的模块

cargo test <optional: test_name>

2:模拟 runtime 环境

需要配置 configuration type,是一个 rust 里的枚举变量。

frame_support::construct_runtime!(

    pub enum Test where
        Block = Block,
        NodeBlock = Block,
        UncheckedExtrinsic = UncheckedExtrinsic,
    {
        System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
        TemplateModule: pallet_template::{Pallet, Call, Storage, Event<T>},
    }
);

impl frame_system::Config for Test {
    // -- snip --
    type AccountId = u64;
}

3:模拟 runtime storage

使用 TestExternalities 来实现,用于测试里的模拟存储。是 substrate_state_machine 里的一个基于 hashhmap 的外部实现的别名。

pub struct ExtBuilder;

//ExtBuilder是TestExternalities的一个实例
impl ExtBuilder {
    pub fn build(self) -> sp_io::TestExternalities {
        let mut t = system::GenesisConfig::default().build_storage::<TestRuntime>().unwrap();
        let mut ext = sp_io::TestExternalities::new(t);
        ext.execute_with(|| System::set_block_number(1));
        ext
    }
}

搭配一个创世配置可以构建模拟的运行环境。

pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {

    pub balances: Vec<(T::AccountId, T::Balance)>,
}

4:区块生产

通过模拟区块生产来验证预期行为在区块生产中是否成立。

//在所有以System::block_number()为唯一输入的模块的on_initialize和on_finalize调用之间递增System模块的块号

//

fn run_to_block(n: u64) {

    while System::block_number() < n {
        if System::block_number() > 0 {
            ExamplePallet::on_finalize(System::block_number());
            System::on_finalize(System::block_number());
        }
        System::set_block_number(System::block_number() + 1);
        System::on_initialize(System::block_number());
        ExamplePallet::on_initialize(System::block_number());
    }
}
#[test]

fn my_runtime_test() {
    with_externalities(&mut new_test_ext(), || {
        assert_ok!(ExamplePallet::start_auction());
        run_to_block(10);
        assert_ok!(ExamplePallet::end_auction());
    });
}

5:对 events 进行测试

#[test]

fn subtract_extrinsic_works() {
    new_test_ext().execute_with(|| {
        assert_ok!(<test_pallet::Pallet<Test>>::subtract(Origin::signed(1), 42, 12));
        let event = <frame_system::Pallet<Test>>::events().pop()
            .expect("Expected at least one EventRecord to be found").event;
        assert_eq!(event, mock::Event::from(test_pallet::Event::Result(42 - 12)));
    });
}

Randomness

确定性的随机性 Deterministic randomness:看似是随机的,其实是根据密码学而无法预测的伪随机值。
Substrate 的随机性特质:trait Randomness 提供了生成随机性的逻辑和消耗随机性的逻辑的接口。

Consuming randomness:类似于 python 里的 random(),1:random_seed,同一个 block 里多次调用结果一样 2:random,随机出来的结果对上下文来说是独特的。

Generating randomness:Substrate 有两种实现随机性特征的方法。1:the Randomness Collective Flip Pallet:基于集体掷硬币,性能高,不安全,只能在测试中使用 2:the BABE pallet, 使用可验证的随机函数,可以提供生产级别的随机性,并在 Polkadot 中使用。选择这个随机性源决定了你的区块链使用 Babe 共识。(?)

Security properties:随机性为 substrate 运行提供了一个方便实用的方法,但是没有保证安全性,需要开发人员手动保证使用的随机源可以满足所有需要消耗随机性的 pallet 的要求。

Chain Specification

是一个配置信息的集合,规定区块链节点将连接到哪个网络,它最初将与哪些实体进行通信,以及它在创建时必须具备哪些 consensus-critical state。

包含的结构:

  1. ClientSpec:包含 client 需要的配置信息,这些信息大部分用于与网络中的其他各方进行通信。(创世后可修改)

  2. The genesis state:consensus-critical genesis configuration,必须在这个初始状态上达成一致,然后才能就任何后续区块达成一致。因此,这个信息必须在链的一开始就建立,此后如果不启动一个全新的区块链就不能改变。没有命令行标志可以覆盖链规范的创世部分的值。

存储方式:

  1. Rust 代码,存储在每个 client 里,这样可以确保节点知道如何连接到至少一个链(不需要额外的信息)

  2. json 文件(所以可以直接上传一个 json 文件创建一个区块链)

如何启动一个链:

Node operator 每次启动一个节点的时候,都提供一个 chain specification, 被硬编码斤二进制文件里。这个 chain specification 可以被导出成一个 json 文件,也可以导入这个 json 文件创建一个标准化链。

substrate build-spec > myCustomSpec.json

substrate --chain=myCustomSpec.json

Upgrade

The System library defines the set_code call that is used to update the definition of the runtime.

The Forkless Upgrade a Chain tutorial describes the details of FRAME runtime upgrades and demonstrates two mechanisms for performing them.

Tutorial: Initiate a Forkless Runtime Upgrade | Substrate_

Runtime versioning

pub const VERSION: RuntimeVersion = RuntimeVersion {
  spec_name: create_runtime_str!("node-template"),
  impl_name: create_runtime_str!("node-template"),
  authoring_version: 1,  // 区块产生接口
  spec_version: 1,    // Runtime 版本
  impl_version: 1,    // spec的实现版本
  apis: RUNTIME_API_VERSIONS,  // 一组runtime API 的Version
  transaction_version: 1,
};

一旦 WASM runtime 升级了,会检查 native runtime 的版本是否对应,不一致的话就会去运行 WASM runtme 的 code

Storage migrations

改变已经存在的数据的属性

通过 FRAME 改变:

FRAME storage migrations are implemented by way of the OnRuntimeUpgrade trait, which specifies a single function, on_runtime_upgrade

Coupling Pallets

tight coupling(两组类之间相互依赖) , loose coupling(一个类使用另一个类的 pub 接口)

代码解读

Substrate 代码导读:node-template - Kaichao 的文章 - 知乎

https://zhuanlan.zhihu.com/p/123167097

创建或引入一个包进入 Runtime 的 wasm:

Substrate 入门 - Runtime 的 wasm 与 native -(九) - 金晓的文章 - 知乎

Runtime 依赖树中的 crate 的 Cargo.toml 的编写

Substrate 入门 - Substrate 的模型设计 -(七) - 金晓的文章 - 知乎

虽然在 Substrate 中的 Runtime 是由 Rust 编写,可以编译成为 Rust 的 native 代码及 wasm 代码,但其本质上并非能调用到 Rust 的所有 std 库提供的接口,同时若引入新的库,若只支持 std,不支持 no_std 的库也是无法通过编译的。

[dependencies]
# 在 dependencies 中,一定要有 default-features = false
frame-system = { version = "2.0.0", default-features = false, path = "../system" } 

[features]

default = ["std"] # 默认启用的feature是std
std = [
    "serde",
    # ...
]
  • 在该 crate 的根目录下的 lib.rs 的前几行:
// 若没有在std的条件编译下,则对于该crate采用`no_std`
#![cfg_attr(not(feature = "std"), no_std)]

Runtime 依赖树中的 crate 项目内容的编写

对于这个 crate 项目,需要注意的有以下几点:

  1. lib.rs 文件中的第一行必须有 #![cfg_attr(not(feature = "std"), no_std)]
  2. 引入标准库的类型,如 Vec, ResultBTreeMap 等等,必须通过 sp-std 这个库引入。
  3. 在 Runtime 的依赖库中,不能出现没有在 sp-std 导入的原本 std 拥有的标准库类型及宏,例如 String,宏 println!,若一定要出现,那么需要通过条件编译 #[cfg(feature = "std")] 包起来,那么被条件编译包起来的部分,显然在编译 wasm 的时候不会被编译进去,那么就必须得保证即使 wasm 没有编译这部分逻辑,那么 native 与 wasm 的执行结果也必须保持一致 (例如只有 println 在里面的话,只会产生的在 native 下打印的效果,不会影响执行的结果。但是若是有操作逻辑改变了变量状态在条件编译中,那么是一定要禁止的,否则就会导致节点运行过程中产生不同的结果)

引入一个第三方库兼容 Runtime wasm 的编译环境

请分清引入这个库的作用,确保这份代码的执行必须在 Runtime 内部,若确定只能在 Runtime 内部,那么只能尝试将其改成能满足前面说的条件的情况,并且其一系列依赖也要满足条件,若不确定只在 Runtime 内部运行,那么只把定义抽离出来,将实现通过 runtime_interface 导出到 native 执行
若这个库只需要在 native 下执行(如 serde),那么使用 Optional 引入,只在 std 下编译。
因此若一个第三方库一定要引入 Runtime 的编译依赖中,请再三思量是否是必须要引入的,因为这并非一件简单的事情。一方面引入新的库,编译会造成 wasm 文件庞大(因为会引入很多依赖一同编译),一方面将一个库改造成能在 Runtime wasm 下编译需要很多工作量。

Macro expand 宏展开

Substrate 入门 - 学习 Runtime 必备的技能 -(十一) - 金晓的文章 - 知乎

https://zhuanlan.zhihu.com/p/110185152

cargo expand: https://github.com/dtolnay/cargo-expand

runtime/src/lib.rs

**cargo install cargo-expand**
cd runtime

cargo expand > expand.rs

# expand.rs 中会展开 runtime/src/lib.rs 所有的宏

// 展开前
_// Define the types required by the Scheduler pallet._

parameter_types! {
    _pub_ MaximumSchedulerWeight: Weight = 10_000_000;
    _pub_ _const_ MaxScheduledPerBlock: u32 = 50;
}


// 展开后

_pub_ _struct_ MaxScheduledPerBlock;

_impl_ MaxScheduledPerBlock {
_    /// Returns the value of this parameter type._
    _pub_ _const_ _fn_ get() -> u32 {
        50
    }
}
_impl_<I: From<u32>> ::frame_support::traits::Get<I> _for_ MaxScheduledPerBlock {
    _fn_ get() -> I {
        I::from(Self::get())
    }
}
construct_runtime!(
    _pub_ _enum_ Runtime _where_
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
    {
        ...
    }
// 会递归展开成一段很长的代码

pallets/template/src/lib.rs

Rust 语法知识:

_const_ _: () = {

    _impl_<T> core::cmp::Eq _for_ Pallet<T> {}
};
_// The struct on which we build all of our Pallet logic._
#[pallet::pallet]
#[pallet::generate_store(_pub_(super) _trait_ Store)]
_pub_ _struct_ Pallet<T>(_);


// 展开为

_pub_ _struct_ Pallet<T>(frame_support::sp_std::marker::PhantomData<(T)>);

    // 未命名常量

    _const_ _: () = {
        _impl_<T> core::clone::Clone _for_ Pallet<T> {
            _fn_ clone(&self) -> Self {
                Self(core::clone::Clone::clone(&self.0))
            }
        }
    };
    _const_ _: () = {
        _impl_<T> core::cmp::Eq _for_ Pallet<T> {}
    };
    _const_ _: () = {
        _impl_<T> core::cmp::PartialEq _for_ Pallet<T> {
            _fn_ eq(&self, other: &Self) -> bool {
                true && self.0 == other.0
            }
        }
    };
    _const_ _: () = {
        _impl_<T> core::fmt::Debug _for_ Pallet<T> {
            _fn_ fmt(&self, fmt: &_mut_ core::fmt::Formatter) -> core::fmt::Result {
                fmt.debug_tuple("Pallet").field(&self.0).finish()
            }
        }
    };

Runtime versioning

与无分叉升级有关

// runtime/src/lib.rs
// https://docs.substrate.io/v3/runtime/upgrades/#runtime-versioning

// In order for the [executor](https://docs.substrate.io/v3/advanced/executor/) to be able to select the appropriate runtime execution environment, it needs to know the `spec_name`, `spec_version` and `authoring_version` of both the native and Wasm runtime.

#[sp_version::runtime_version]

_pub_ _const_ VERSION: RuntimeVersion = RuntimeVersion {
    spec_name: create_runtime_str!("node-template"),
    impl_name: create_runtime_str!("node-template"),
    authoring_version: 1,
_    // The version of the runtime specification. A full node will not attempt to use its native_
_    //   runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,_
_    //   `spec_version`, and `authoring_version` are the same between Wasm and native._
_    // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use_
_    //   the compatible custom types._
    spec_version: 102,
    impl_version: 1,
    apis: RUNTIME_API_VERSIONS,
    transaction_version: 1,
    state_version: 1,
};

Rust 学习

Ownership

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.
//错误示范

fn takes_own(s:String){

}
fn main(){

    let s = String::from("he");
    takes_own(s);
    println!("{}", s);
}
//s的生命周期已经在takes_own函数里结束了

//正确

fn takes_own(s:&String){

}
fn main(){

    let s = String::from("he");
    takes_own(&s);
    println!("{}", s);
}

Move & Copy

Default is move.

Copy for a type should implement the Copy trait.

types that implement Copy:

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.

引用和借用

一个地址只能被引用一次,所以已经被 borrow(创建一个引用就叫 borrow)之后,再修改或者引用之后就会报错。

fn main() {

    let s = String::from("hello");

    change(&s);
}
fn change(some_string: &String) {
    some_string.push_str(", world");
}

Mutable/Immutable Reference

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • 我们 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
  • 要么只有一个用户拥有写入权限,要么多个用户

Immutable reference is safe.

Be careful of mutable reference

// Will not compile
fn main() {

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;
    
    println!("{}, {}", r1, r2);
}

Only one mutable reference prevents data race.

// Will compile
let mut s = String::from("hello");


{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

Dangling References

Rust compiler will detect these dangling pointers.

函数应用

  1. windows()函数
let slice = ['r', 'u', 's', 't'];

let iter1 = slice.windows(2);
println!("{:?}",mut iter1.any(|v| v == ['s']));
  1. 排序 PartialOrd trait 特性

特质 Trait

substrate 中大量使用

trait 是对未知类型 Self 定义的方法集。该类型也可以访问同一个 trait 中定义的 其他方法。

  1. 未知类型定义
  2. 未知类型可以访问 trait 中定义方法 -> trait 有点像父类,而未知类型是子类
    与 Java/Golang 中的接口不同点在于:

接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法

编译器自动派生 #[derive]

下面是可以自动派生的 trait:

  • 比较 trait: Eq, PartialEq, Ord, PartialOrd
  • Clone, 用来从 &T 创建副本 T
  • Copy,使类型具有 “复制语义”(copy semantics)而非 “移动语义”(move semantics)。
  • Hash,从 &T 计算哈希值(hash)。
  • Default, 创建数据类型的一个空实例。
  • Debug,使用 {:?} formatter 来格式化一个值。

运算符重载

https://rustwiki.org/zh-CN/core/ops/

例如可以通过 Add trait 重载加法运算符 (+)

use std::ops::{Add, Sub};

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {x: self.x + other.x, y: self.y + other.y}
    }
}

impl Sub for Point {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self {x: self.x - other.x, y: self.y - other.y}
    }
}

assert_eq!(Point {x: 3, y: 3}, Point {x: 1, y: 0} + Point {x: 2, y: 3});
assert_eq!(Point {x: -1, y: -3}, Point {x: 1, y: 0} - Point {x: 2, y: 3});

Iterator trait

Iterator trait 只需定义一个能返回 next(下一个)元素的方法。

struct Fibonacci {
    curr: u32,
    next: u32,
}

// 为 `Fibonacci`(斐波那契)实现 `Iterator`。
// `Iterator` trait 只需定义一个能返回 `next`(下一个)元素的方法。
impl Iterator for Fibonacci {
    type Item = u32;
    
    // 我们在这里使用 `.curr` 和 `.next` 来定义数列(sequence)。
    // 返回类型为 `Option<T>`:
    //     * 当 `Iterator` 结束时,返回 `None`。
    //     * 其他情况,返回被 `Some` 包裹(wrap)的下一个值。
    fn next(&mut self) -> Option<u32> {
        let new_next = self.curr + self.next;

        self.curr = self.next;
        self.next = new_next;

        // 既然斐波那契数列不存在终点,那么 `Iterator` 将不可能
        // 返回 `None`,而总是返回 `Some`。
        Some(self.curr)
    }
}

Impl Trait 作为类型签名

任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没必要了解传入对象有没有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内也无法使用其他的属性与方法。

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}
// 等价于

fn output<T: Descriptive>(object: T) {

    println!("{}", object.describe());
}
// 特性做返回值

fn person() -> impl Descriptive {

    Person {
        name: String::from("Cali"),
        age: 24
    }
}

trait A+ trait B 表示需要同时实现两个 trait

where 可以简化泛型的类型和约束

fn notify(item: impl Summary + Display)
// 等价于

fn notify<T: Summary + Display>(item: T)
// 等价于

fn notify<T>(item: T) where

    T: Summary + Display

使用 trait bound 有条件地实现方法

impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}

// 使用 `where` 从句来表达约束
struct YourType;

impl <A, D> MyTrait<A, D> for YourType where
    A: TraitB + TraitC,
    D: TraitE + TraitF {}

Clone

Clone trait 能帮助我们复制资源。通常,我们可以使用由 Clone trait 定义的 .clone() 方法。

父 trait

Rust 没有“继承”,但是您可以将一个 trait 定义为另一个 trait 的超集(即父 trait)。

trait Person {
    fn name(&self) -> String;
}

// Person 是 Student 的父 trait。
// 实现 Student 需要你也 impl 了 Person。
trait Student: Person {
    fn university(&self) -> String;
}

trait Programmer {
    fn fav_language(&self) -> String;
}

// CompSciStudent (computer science student,计算机科学的学生) 是 Programmer 和 Student 两者的子类。
// 实现 CompSciStudent 需要你同时 impl 了两个父 trait。
trait CompSciStudent: Programmer + Student {
    fn git_username(&self) -> String;
}

fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
    format!(
        "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
        student.name(),
        student.university(),
        student.fav_language(),
        student.git_username()
    )
}

fn main() {}

关联类型

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

高级 trait - Rust 程序设计语言 中文版

宏(Macro)

  • 元编程(metaprogramming)
  • 宏并不产生函数调用,而是展开成源码,并和程序的其余部分一起被编译。
    宏的用处:
  1. 不写重复代码(DRY,Don’t repeat yourself.)。很多时候你需要在一些地方针对不同的类型实现类似的功能,这时常常可以使用宏来避免重复代码。
  2. 领域专用语言(DSL,domain-specific language)。宏允许你为特定的目的创造特定的语法。
  3. 可变接口(variadic interface)。有时你需要能够接受不定数目参数的接口,比如 println!,根据格式化字符串的不同,它需要接受任意多的参数。

使用宏而不是编写宏

可以采取可变数量的参数:https://rustwiki.org/zh-CN/rust-by-example/macros/variadics.html

声明式宏(Declarative macros):更类似于 match 的匹配而不是函数定义

#![allow(unused)]
fn main(
    let v: Vec<u32> = vec![1, 2, 3];
}

泛型

泛型可以用来代表各种各样可能的数据类型,泛型之于数据类型,类似于变量之于内存数据。

减少很多冗余代码

use std::ops::Add;

fn double<T>(i: T) -> T
  where T: Add<Output=T> + Clone + Copy {//where 
  i + i
}
fn main(){
  println!("{}",double(3_i16));
  println!("{}",double(3_i32));
}

生命周期

生命周期与引用有效性 - Rust 程序设计语言 中文版

Rust 所有权语义模型

fn longest(x: &str, y: &str) -> &str {

    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {

    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

上面会因为 返回值引用可能会返回过期的引用(悬垂引用) 而报错

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

现在函数签名表明对于某些生命周期 'a,函数会获取两个参数,他们都是与生命周期 'a 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 'a 存在的一样长的字符串 slice。它的实际含义是 longest 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。这就是我们告诉 Rust 需要其保证的约束条件。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest 函数并不需要知道 xy 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名。

// 编译错误
fn main() {

    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1 更长,因此 result 会包含指向 string1 的引用。因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。

生命周期注释是描述引用生命周期的办法。

&i32        // 常规引用

&'a i32     // 含有生命周期注释的引用
&'a mut i32 // 可变型含有生命周期注释的引用

对于上面那个例子,需要修改 longer 函数的

use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这个是示例 10-22 中那个返回两个字符串 slice 中较长者的 longest 函数,不过带有一个额外的参数 annann** 的类型是泛型 T,它可以被放入任何实现了 where 从句中指定的 Display trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 Display trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 ‘a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。**

错误处理 - 通过例子学 Rust 中文版

  • panic 主要用于测试,以及处理不可恢复的错误
  • Option 表示值存在或不存在
  • Result 当错误有可能发生,且应当由调用者处理时

OptionResult 都具有的方法:

  • unwrap() option 为 None 时/result 为 Err 时执行 panic
  • ?
  • option 为 None 时,终止函数执行且返回 None,否则返回 Some 值
  • result 为 Err 时,终止函数执行且返回 Err,否则返回 Ok 值

Option

在标准库(std)中有个叫做 Option<T>(option 中文意思是 “选项”)的枚举 类型,用于有 “不存在” 的可能性的情况。它表现为以下两个 “option”(选项)中 的一个:

Result

ResultOption 类型的更丰富的版本,描述的是可能的错误而不是可能的不存在

也就是说,Result<T,E> 可以有两个结果的其中一个:

  • Ok<T>:找到 T 元素
  • Err<E>:找到 E 元素,E 即表示错误的类型。

项目管理

如何构建一个项目

属性 attribute

Substrate 里面大量使用

  • 属性是应用于某些模块、crate 或项的元数据(metadata)
  • 当属性作用于整个 crate 时,它们的语法为 #![crate_attribute],当它们用于模块或项时,语法为 #[item_attribute](注意少了感叹号 !)。
  • 属性可以接受参数,有不同的语法形式:
  • #[attribute = "value"]
  • #[attribute(key = "value")]
  • #[attribute(value)]

Lint

  • #[allow(dead_code)] 编译器提供了 dead_code(死代码,无效代码)_lint_,这会对未使用的函数 产生警告。可以用一个属性来禁用这个 lint。

条件编译

可能通过两种不同的操作符实现:

  • cfg 属性:在属性位置中使用 #[cfg(...)]
  • cfg! 宏:在布尔表达式中使用 cfg!(...)
// 这个函数仅当目标系统是 Linux 的时候才会编译
#[cfg(target_os = "linux")]
fn are_you_on_linux() {
    println!("You are running linux!")
}

// 而这个函数仅当目标系统 **不是** Linux 时才会编译
#[cfg(not(target_os = "linux"))]
fn are_you_on_linux() {
    println!("You are *not* running linux!")
}

fn main() {
    are_you_on_linux();
    
    println!("Are you sure?");
    if cfg!(target_os = "linux") {
        println!("Yes. It's definitely linux!");
    } else {
        println!("Yes. It's definitely *not* linux!");
    }
}

自定义条件:

#[cfg(some_condition)]
fn conditional_function() {
    println!("condition met!")
}

fn main() {
    conditional_function();
}

Workspace 工作区

清单格式 - Cargo 手册 中文版

Features 条件编译和可选依赖

https://rustwiki.org/zh-CN/cargo/reference/features.html

// 表示当 feature = "std" 为 false时,设置 no_std 属性
#![cfg_attr(not(feature = "std"), no_std)]

条件编译

https://rustwiki.org/zh-CN/reference/conditional-compilation.html?highlight=cfg_attr#cfg_attr%E5%B1%9E%E6%80%A7

面向对象

https://llever.com/gentle-intro/object-orientation.zh.html

  • 封装:通过模块 mod 实现
  • 每一个 Rust 文件都可以看作一个模块
  • 类 class 对应 Rust 中的 structenum
  • 继承:通过 trait 实现部分
  • trait 只让类型继承了方法,没有继承变量
  • “如果它嘎嘎叫,那就是鸭子”。只要实现了 嘎嘎{quacks} 方法,就代表该类型是鸭子
#![allow(unused_variables)]
fn main() {
trait Quack {
    fn quack(&self);
}

struct Duck ();

// 鸭子会嘎嘎叫

impl Quack for Duck {
    fn quack(&self) {
        println!("quack!");
    }
}



struct RandomBird {
    is_a_parrot: bool
}


// 鸟也会嘎嘎叫
impl Quack for RandomBird {
    fn quack(&self) {
        if ! self.is_a_parrot {
            println!("quack!");
        } else {
            println!("squawk!");
        }
    }
}

let duck1 = Duck();
let duck2 = RandomBird{is_a_parrot: false};
let parrot = RandomBird{is_a_parrot: true};

// 把它们都当成鸭子,因为它们都会嘎嘎叫

let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot];

for d in &ducks {
    d.quack();
}
// quack!
// quack!
// squawk!
}

反转链表——一图胜千言

剑指 Offer 24. 反转链表

上次面试没做出来这题,尬住了

        1 -> 2 -> 3 -> 4 -> 5 ->NULL
NULL <- 1 <- 2 <- 3 <- 4 <- 5

对1来说,从NULL -> 1变成1 -> NULL
对2来说……

因此,我们需要用pre变量存储原链表的上一个节点,也就是新链表的下一个节点

思路明确后,剩下的过程就很简单啦。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        while (head != nullptr) {
            ListNode *nxt = head->next;
            head->next = pre;
            pre = head;
            head = nxt;
        }
        return pre;
    }
};

剑指 Offer 09. 用两个栈实现队列

https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/

这题做了好几遍了,但是每次做的时候都要思考一会,在此将思路记下来。

用测试驱动开发TDD的思路做题

  • 队列是FIFO,栈是FILO
  • 每次入队的时候只能将元素保存在栈中,即stack.push()
  • 每次出队的时候只能将元素出栈,使用stack.pop()
  • 现在我们有两个栈,要实现队列FIFO的功能,自然想到要将入队元素保存在第一个栈(入队栈)中,将出队元素保存在第二个栈(出队栈)中。
  • 元素入队比较容易处理,将其加到入队栈即可。
  • 元素出队则需要仔细思考:怎么保证每次出队的都是最先入队的元素呢?
    • 先构建一个简单的测试样例:三个元素1,2,3先后入队,入队栈:[1,2,3]
      • 那只要依次将入队栈的元素pop到出队栈即可,出队栈变为:[3,2,1],这样出队栈每次pop的就是最先入队的元素。
      • 根据这种情况,我们总结出的算法是:每次元素出队时,都依次将入队栈的元素pop到出队栈,再让出队栈pop。
    • 让我们构建另一个测试样例:1,2先后入队,出队一次,3入队,出队一次。
      • 使用上面总结出的算法,我们会发现出队元素是1,3,而正确结果应该是1,2。错误的原因是32在出队栈中的顺序调转了,在3入队+出队一次后,3居然成为了出队栈的栈顶元素。
      • 认真思考一下,出队栈的元素顺序在一轮依次将入队栈的元素pop到出队栈中才能得到保护,如果有多轮依次将入队栈的元素pop到出队栈,那么出队栈的元素顺序会被破坏。
      • 因此,只有在出队栈为空时,才进行依次将入队栈的元素pop到出队栈的操作。若出队栈不为空,直接让出队栈pop即可。
class CQueue {
public:
    std::stack<int> a,b;
    CQueue() {
    }
    
    void appendTail(int value) {
        a.push(value);
    }
    
    int deleteHead() { 
        // 若出队栈不为空,直接pop
        if (!b.empty()) {
            int head = b.top();
            b.pop();
            return head;
        }
        // 出队栈为空,依次将入队栈的元素pop到出队栈
        while (!a.empty()) {
            b.push(a.top());
            a.pop();
        }
        if (!b.empty()) {
            int head = b.top();
            b.pop();
            return head;
        }
        return -1;
    }
};

晶泰科技实习经历

在晶泰科技的实习经历: 2021.6-2021.12

这是我第一段在公司中的工作经历,在此感谢所有一起工作过的同事~

Python 后端开发

前四个月做的是 Python 后端开发,这也是当时的我唯一能胜任的工作了。在来公司之前,我熟悉的是Django+MySQL这一套,因此一开始还不太习惯写Flask。再加上要学习新的图数据库Neo4j,当时还是有一些不适应的。开始我是跟同事结对编程,在一两周之后就可以独立开发了。

重构

现在回忆起来,其实第一个项目复杂度蛮高的,需要对该领域有一定程度的理解。再加上一开始写后端的时候分层没有做好,当新需求到来的时候就发现系统越来越难写。接口层(api)直接接到数据库层(db)去了,没有在中间加上一个模型层(model)。于是tutor带着我一起重构了系统。这个系统甚至需要一个校验层,因为接口输入和输出的处理本身就很复杂,包含了不少业务逻辑。

重构当然不能少不了自动化测试。重构的过程就像是在走钢丝,如果没有之前写好的一些单元测试和接口测试,那在重构过程中就很可能使系统产生bug。当时公司还组织了一次敏捷开发的工作坊,授课老师提到了一个小步快走的策略:改一点代码,跑一遍测试,改一点代码,跑一遍测试…… 就这样重复下去。当时也看了《重构:改善既有代码的设计》这本经典书籍,从中得到了不少启发,其中最重要的一点就是重构会带来更好的性能。虽然重构代码可能会将一些性能高的写法优化成可读性好的写法(比如把一个for循环拆成两个),但是可读性好的代码更容易被优化。而且在一般的业务开发场景中,性能远没有你想象中的那么重要,在遇到性能问题的时候再去考虑优化代码运行效率也不迟。

敏捷开发

以前我从来没有听说过敏捷开发这个概念,来到公司后体验了一两个月的敏捷开发。每天早上十分钟站会,把看板上面的任务过一遍,每个人都报告一下自己昨天的任务进展,跟大家同步开发进度。每两周开一次总结回顾会和需求计划会,首先总结上两周任务完成情况,再一起规划下两周要做的任务。在我看来,这种开发方式真的挺神奇的,你可以知道同事们都在忙些什么(尽管听完之后可能也不太懂),知道整个团队的重点开发方向。Code Review 也是敏捷开发的一环,这个过程可以帮我们发现很多代码里面的问题。

DevOps

来到公司之后,我才切身体会到CI/CD带给程序员的巨大生产力提升。之前在校内创业公司的时候,后端都是需要自己登上服务器手动部署的,而且从来也没有什么自动化测试来保证交付质量。公司里面有Jenkins做自动化测试、自动化镜像打包,有一键部署镜像到集群上的平台。

Docker

云原生时代怎么能不懂点容器技术呢?

我是在学习CI/CD的过程中踏入容器技术的大门的。当时看的是Docker — 从入门到实践这本书,认真读完一遍后就基本了解了容器的概念及用法。之后就开始给项目写Dockerfile和DockerComposeFile,实现一键启动后端及其依赖服务,切身体会到容器带来的便利。后来做了一个管理Docker Compose的小后端,在开发过程中反复读Docker文档,更令我加深了对容器技术的了解。

SDK

第一个项目需要给其他同事提供SDK,包括离线和在线版本的。当时就在想怎么才能减少维护SDK的工作量,因为在线SDK的代码基本只是调用了接口,完全可以用一些代码生成工具解决的。摸索了好几天,最后在CI中集成了 Swagger Codegen ,通过 Swagger 生成在线版本的SDK。离线版本就只是把一部分代码复制过去改动一点点。在开发第一版SDK的过程中,由于是第一次写Python Package,踩了不少坑。后来有了新的需求,两个版本的SDK不方便使用,我们把它们合成了一个,还加上了缓存的功能以减少服务压力。

API Doc

前后端协作怎么能离开API文档呢?

  • 开发第一个项目的时候用的是Swagger,API文档就放在代码仓库里面一起维护。起初我还觉得挺好的,后端先跟前端约定好接口规范再开发,这样能减少很多前后端对接的时间。后来,当开发速度加快以后,我发现API文档跟代码不同步了,有时候只更新了代码,却忘了更新API文档。不仅如此,前端同事提出了接口用例和接口Mock的需求。
  • 于是我们前后端约定使用Insomnia来进行接口定义和调试,每次更新文档或者接口用例之后就把配置文件导出后发给对方。这种做法仍未解决接口Mock的问题,而且也不便于版本管理。后来,我们觉得在群里发配置文件的方式很麻烦,想要一种更便捷的同步文档的方式。
  • 于是在经过技术选型后,部门部署了YApi,同时解决了文档同步和接口Mock这两个需求。我们在第二个项目中就完全采用了YApi,接口定义、接口Mock、接口测试都在上面做。它不需要我们写Swagger,大部分操作都是傻瓜式的。这既是优点也是缺点:优点是学习成本低,便于上手;缺点就是它无法提供像Swagger一样完善的功能,比如由于YApi不支持定义schema,每次修改model的字段时都要改好几个接口。YApi的接口测试还是十分实用的,可以自动检测接口返回的数据结构是否符合文档定义,同时也支持测试有依赖关系的一组接口。总体来说,YApi还是比较好用的。

Golang 后端开发

最后两个月我转去做Golang开发。从零开始学Golang挺有意思的,从学习它的语法到学习它的设计哲学。我推荐看官方的Go TourEffective Go,认真学习完这两个基本就能开发简单的项目了。如果想继续深入了解底层原理,可以看Go 语言设计与实现
我当时跟着同事开发一个新项目,在实践中学习一些高并发架构设计的经验以及Golang的开发规范,这样会比自学效率高很多。同事帮我Review代码也让我少走了很多弯路。

并发安全

Golang开发一定绕不开并发安全这个话题,其实哪怕是Python开发也会涉及到并发安全。一般人的思维都是串行的,难以推测出程序在并发场景下的行为。在写有一致性要求的代码时,一定要考虑好不同线程/进程间的同步机制。该加锁的地方就得加,不要自作聪明想着这个地方应该不会存在 race condition。正如The Go Memory Model这里写到的:If you must read the rest of this document to understand the behavior of your program, you are being too clever. Don't be clever. 不要自作聪明觉得这里可以无锁编程,只要涉及并发的地方都要加上同步原语(如锁、信号量)。加锁一方面可以确保程序是以正确的顺序运行,另一方面也是告诉其他程序员这块区域是critical section,改动时要特别小心。虽然加锁会带来一定程度上的性能损失,但请记住这句话:在一般的业务开发场景中,性能远没有你想象中的那么重要,在遇到性能问题的时候再去考虑优化代码运行效率也不迟。

总结

时间过得飞快,眨眼间我也在业界工作了半年了。工作没有想象中的有趣,但也还可以吧,起码自己大部分时候还是怀有工作热情的。希望自己早日找到钱多事少离家近的工作吧😂。

一个Python包是如何产生的?

我们都用过pip install,但是你尝试过自己打包一个Python包并提供给他人下载使用吗?

本文提供了一个使用setuptools+click实现的CLI示例,以帮助读者了解Python包的生成过程。

代码已发布在GitHub上:https://github.com/doutv/Python-Package-Demo

目录架构及文件内容

这是一个根据一定的语法规则翻译字符串的CLI,项目并未展示全部细节,只展示了与本文有关的部分代码。

项目目录架构是这样的:

.
│  setup.py
│
└─myproject
    │  api.py
    │  README.md
    │  __init__.py
    │
    ├─data
    │      data.yaml
    │      __init__.py
    │
    └─utils
            common.py
            __init__.py

setup.py

setuptools使用的配置,setup的参数很多,以下是部分参数的解释:

  • packages 项目中要安装的包的名字
  • install_requires 项目的依赖包
    • 可指定版本,使用==,<=,!=等等
  • entry_points CLI命令与函数的对应关系
    • api=myproject.api:cli
    • 当我在命令行输入api时,就会调用myproject.api中的cli函数
  • package_data 要包含的静态文件
# setup.py
from setuptools import setup, find_packages

setup(
    name='myproject',
    version='0.1',
    author="",
    author_email="",
    packages=find_packages(),
    install_requires=[
        'Click==8.0.1',
        'ruamel.yaml==0.17.10'
    ],
    entry_points='''
        [console_scripts]
        api=myproject.api:cli
    ''',
    package_data={
        "": ["*.yaml"]
    }
)

api.py

暴露出来的CLI接口,click库的具体用法请参考官方文档

比较简单的理解:

  • click.group() 可以把多个函数绑定到一个函数上面,这样就可以实现多个command
  • click.argument() 是必填参数
  • click.option() 是选填参数
# api.py
from myproject.utils.common import prepare_init_data, get_rule, parse_data_by_rule
import click


@click.group()
def cli():
    prepare_init_data()


@cli.command()
@click.argument('data')
@click.option('--schema', default="无")
def parse(data, schema):
    print(f"parsing {data} in {schema}")
    print(f"result is {parse_data_by_rule(data)}")


@cli.command()
def rule():
    print(get_rule())

common.py

api.py中需要用到的一些辅助函数

# common.py
RULE = {}


def prepare_init_data():
    from ruamel.yaml import YAML
    try:
        import importlib.resources as pkg_resources
    except ImportError:
        # Try backported to PY<37 `importlib_resources`.
        import importlib_resources as pkg_resources
    from myproject import data
    yaml = YAML()
    filename = "data.yaml"
    in_data = pkg_resources.read_text(data, filename)
    global RULE
    RULE = yaml.load(in_data)


def parse_data_by_rule(data):
    global RULE
    return RULE.get(data)


def get_rule():
    global RULE
    return RULE

调试及安装

  • 在Debug Mode下调试: pip install -e .
  • 在本地安装: python setup.py install

调用方法:

  1. 在任意路径的命令行输入api --help
    1. api parse --help
    2. api rule --help
  2. 安装成功后,可以在别的Python包中import myproject使用

发布到PyPi或Conda源上

请参考网络上的其他文章

References

Python 后端开发学习路线

TeaBreak 后端知识库搬运过来的

目标读者是零基础的学生

Week 1 实用工具及技巧

Git和Markdown是必须学习的内容

Tasks

  • 在GitHub上加入TeaBreak Team并创建你的学习笔记仓库
  • 用Markdown完成你的第一篇学习笔记

Week 2 Python语法 & Django入门

Python语法

官方文档:https://docs.python.org/3/

廖雪峰的python教程:https://www.liaoxuefeng.com/wiki/1016959663602400

  • 数据类型
  • 流程控制
    • 条件判断
    • 循环
  • 函数
  • 数据结构
    • 列表
    • 字典
  • 异常处理
  • 模块和包
    • 以下三种import的区别:
      • import json
      • from json import *
      • from json import load
    • 类成员
    • 类方法
  • 装饰器(选学)

Django入门

官方文档:https://docs.djangoproject.com/zh-hans/3.0/

在实践中学习Django会比较快,哪里不会查哪里,首先在官方文档里找,其次用Google搜索。

  1. 推荐大家先跟着官方教程做一个简单的应用:https://docs.djangoproject.com/zh-hans/3.0/intro/tutorial01/
  2. 在我们团队中,Django仅作为后端使用,且在Django基础上使用了DRF (Django REST Framework)这个后端专用框架。DRF的写法与Django原生的写法差别挺大的,但是这两种写法是可以同时存在的,建议大家先熟悉Django原生的写法,再弄懂DRF帮我们节省了哪些部分的代码工作
  3. 推荐大家阅读迎新墙课程二维码的代码来熟悉Django的使用:
    1. models, views, urls 三部分各自的作用及联系
    2. 各个.py文件的作用
    3. settings.pylocal_settings.py 中的配置
    4. Admin后台管理模块:迎新墙
    5. 数据库增删查改 QuerySet
    6. Django 中使用 MySQL 数据库:
      • 在本地安装 MySQL
      • 安装 MySQL Workbench
      • 在 Django 项目的 models.py 中定义数据库表
      • 数据库表结构的设计是一大学问,现阶段只需要保证信息完整即可
      • 可参考迎新墙的数据库设计
        • 主键 primary_key 是每一行数据的唯一标识
        • 外键 foreign_key 连接两个表(现阶段不用考虑,活动日历建一个表即可)
        • Django ORM 常用字段类型
          • AutoField 自增主键
          • CharField 定长字符串
          • DateTimeField 日期时间
          • PositiveIntegerField 非负整数
          • TextField 长字符串
      • 在 Django 项目的 settings.py 中定义数据库连接:
      •   DATABASES = {
              'default': {
                  'ENGINE': 'django.db.backends.mysql',
                  'OPTIONS': {
                      "host": "127.0.0.1",                    # 本地的MySQL
                      "port": 3306,
                      "database": "database_name",            # 改成你的数据库名字
                      "user": "your_database_username",       # 用户名
                      "password": "your_database_password",   # 密码
                  },
              }
          }
        
      • python manage.py make migrations xxx(项目名字) 创建数据库迁移文件
      • python manage.py migrate 数据库迁移
      • 数据库增删查改的操作,请参考QuerySet API问答墙的API
  4. 推荐大家阅读柠檬茶日历用户系统的后端代码来熟悉DRF的使用:
    1. 阅读后端README
    2. http返回状态码及信息:
      1. Response(msg="not found",status=status.HTTP_404_NOT_FOUND) 显式定义状态码
    3. 装饰器
      1. 原理:https://segmentfault.com/a/1190000004461404
      2. 应用:
        1. @api_view(["POST"]) 指定某个接口的http方法
        2. @permission_classes([]) DRF中的权限管理
        3. @authentication_classes([CustomUserTokenAuth]) DRF中的用户认证
    4. 欢迎大家来完善原有的后端代码:在企业微信里提出建议 -> 新建分支 -> 在新分支上变更 -> Pull Request 或 直接合并到master

Tasks

Week 3 如何在服务器上部署代码

详见后端开发流程:https://github.com/TeaBreak-Tech/Backend-Learning/blob/master/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91%E6%B5%81%E7%A8%8B.md

  • SSH
  • 常用命令
    • ls -al or ll alias
    • pwd
    • cd
    • screen
      • screen -ls
      • screen -x <name>
      • <Ctrl-a> <Ctrl-d>
    • top or htop
    • mv
    • cp
  • 怎么将代码部署到服务器上
    • Git同步或scp同步
    • Nginx反向代理
    • runserver

Week 4 Django REST framework (DRF)

柠檬茶日历为例

Week 5 User System Design

Week 7 Nginx

  • nginx.conf语法
  • e.g calendar nginx.conf
  •     server {
            listen 443 ssl;
            server_name calendar.dev.tea-break.cn;
            ssl_certificate /home/www/calendar/SSL/dev/Nginx/1_calendar.dev.tea-break.cn_bundle.crt;
            ssl_certificate_key /home/www/calendar/SSL/dev/Nginx/2_calendar.dev.tea-break.cn.key;
            ssl_session_timeout 5m;
            ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
            ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
            ssl_prefer_server_ciphers on;
    
            location /django/static/ {
                alias /home/www/calendar/django/static/;
            }
            location / {
                root /home/www/calendar/react/build;
                index  index.html index.htm;
                try_files $uri /index.html
                autoindex on;
            }
            location /api {
                proxy_pass  http://127.0.0.1:8004;
            }
            location /uauth {
                proxy_pass  http://127.0.0.1:9000;
            }
            location /api/get_cos_credential {
                proxy_pass  https://www.tea-break.cn;
            }
            location /T67xT93qKR.txt {
                root /;
                expires -1;
                add_header Cache-Control no-store;
            }
        }
    
        server {
            listen       80;
            server_name  calendar.dev.tea-break.cn;
            return 301 https://$host$request_uri;
        }
    
    
        server {
            listen 443 ssl;
            server_name app.calendar.dev.tea-break.cn;
            ssl_certificate /home/www/calendar/SSL/app/Nginx/1_app.calendar.dev.tea-break.cn_bundle.crt;
            ssl_certificate_key /home/www/calendar/SSL/app/Nginx/2_app.calendar.dev.tea-break.cn.key;
            ssl_session_timeout 5m;
            ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
            ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
            ssl_prefer_server_ciphers on;
    
            location / {
                root /home/www/calendar/app/build;
                index  index.html index.htm;
                try_files $uri /index.html
                autoindex on;
            }
            location /api {
                proxy_pass  http://127.0.0.1:8004;
            }
            location /uauth {
                proxy_pass  http://127.0.0.1:9000;
            }
            location /api/get_cos_credential {
                proxy_pass  https://www.tea-break.cn;
            }
            location /T67xT93qKR.txt {
                root /;
                expires -1;
                add_header Cache-Control no-store;
            }
        }
    
        server {
            listen       80;
            server_name  app.calendar.dev.tea-break.cn;
            return 301 https://$host$request_uri;
        }
    
0%