2023 Advent-of-spin Challenge 3

이 포스트는 advent-of-spin에 업로드 된 Challenge 3에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • /POST method 구현하기
    • POST: / body로 아래의 형식대로 값을 전달
         {
             "place": "North Pole",
             "characters": ["Santa Claus", "The Grinch", "a pingvin"],
             "objects": ["A spoon", "Two presents", "Palm tree"]
         }
      
    • 결과 값으로 제공된 데이터 세트에 대해서 LLM을 사용하여 크리스마스와 관련된 이야기를 만들어 출력 json { "story": "<YOUR STORY HERE>" }
    • 상태코드 200으로 반환
    • response header에 Content-Type: application/json

Work

  • 이번 챌린지는 단순하게 POST 메소드로 라우팅을 하나만 뚫으면 되는 간단한 챌린지 입니다.
  • spin new 명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.

    spin new

  • 그리고 생성된 프로젝트의 Cargo.toml 파일에 아래와 같이 dependencies를 추가해 줍니다.

    dependencies

    • request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
  • 이번 챌린지에선 LLM을 사용하므로 관련 설정을 spin.toml 에 추가합니다.
          [component.challenge]
          source = "target/wasm32-wasi/release/challenge.wasm"
          allowed_outbound_hosts = []
          ai_models = ["llama2-chat"]
          [component.challenge.build]
          command = "cargo build --target wasm32-wasi --release"
          watch = ["src/**/*.rs", "Cargo.toml"]
    
  • src/lib.rs 에 POST를 처리하기 위한 로직을 구현합니다.

      use http::{Method, StatusCode};
      use serde::{Deserialize, Serialize};
      use spin_sdk::{
          http::{IntoResponse, Response},
          http_component, llm,
      };
    
      #[derive(Debug, Deserialize, Serialize)]
      struct StoryRequest {
          place: String,
          characters: Vec<String>,
          objects: Vec<String>,
      }
    
      #[derive(Debug, Deserialize, Serialize)]
      struct StoryResponse {
          story: String,
      }
    
      fn make_story(place: String, characters: Vec<String>, objects: Vec<String>) -> String {
          let model = llm::InferencingModel::Llama2Chat;
          let prompt = format!(
              "Can you make story with place {:?} and these characters {:?} and this objects {:?}",
              place, characters, objects
          );
          let inference = llm::infer(model, &prompt);
          format!("{:?}", inference)
      }
    
      #[http_component]
      fn handle_request(req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
          let (status, body) = match *req.method() {
              Method::POST => {
                  let request: StoryRequest =
                      serde_json::from_slice(req.body().clone().to_vec().as_slice()).unwrap();
    
                  let place = request.place;
                  let characters = request.characters;
                  let objects = request.objects;
    
                  let story = make_story(place, characters, objects);
    
                  let response = StoryResponse { story: story };
    
                  let json_response = serde_json::to_string(&response)?;
    
                  (StatusCode::OK, json_response)
              }
              _ => (StatusCode::METHOD_NOT_ALLOWED, String::new()),
          };
          Ok(Response::builder()
              .status(status)
              .header("content-type", "application/json")
              .body(body)
              .build())
      }
    

2023 Advent-of-spin Challenge 2

이 포스트는 advent-of-spin에 업로드 된 Challenge 2에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • /POST method 구현하기
    • POST: / body로 아래의 형식대로 값을 전달
         {
             "kids": [1, 4 , 5, 6, 3, 2, 7],
             "weight": [23, 45, 23, 43, 12, 32, 45],
             "capacity": 120
         }
      
    • 결과 값으로 제공된 데이터 세트에 대해서 얼마나 많은 Kids에게 선물을 줄 수 있는지 출력 json { "kids": 18 }
    • response header에 Content-Type: application/json

Work

  • 이번 챌린지는 단순하게 POST 메소드로 라우팅을 하나만 뚫으면 되는 간단한 챌린지 입니다.
  • spin new 명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.

    spin new

  • 그리고 생성된 프로젝트의 Cargo.toml 파일에 아래와 같이 dependencies를 추가해 줍니다.

    dependencies

    • request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
  • 이제 구현을 해야합니다.
  • 문제를 해결하기 위해서 DP를 사용하여 로직을 구현할 수 있습니다.

      fn optimal_kids_reached(kids: Vec<usize>, weight: Vec<usize>, capacity: usize) -> usize {
          let n = kids.len();
          let mut dp = vec![vec![0; capacity + 1]; n + 1];
        
          for i in 1..=n {
              for w in 1..=capacity {
                  if weight[i - 1] <= w {
                      dp[i][w] = dp[i - 1][w].max(kids[i - 1] + dp[i - 1][w - weight[i - 1]]);
                  } else {
                      dp[i][w] = dp[i - 1][w];
                  }
              }
          }
        
          dp[n][capacity]
      }
    
  • src/lib.rs 에 POST를 처리하기 위한 로직을 구현합니다.

      use http::{Method, StatusCode};
      use serde::{Deserialize, Serialize};
      use spin_sdk::{
          http::{IntoResponse, Response},
          http_component,
      };
        
      #[derive(Debug, Deserialize, Serialize)]
      struct KidsRequest {
          kids: Vec<usize>,
          weight: Vec<usize>,
          capacity: usize,
      }
        
      #[derive(Debug, Deserialize, Serialize)]
      struct KidsResponse {
          kids: usize,
      }
        
      fn optimal_kids_reached(kids: Vec<usize>, weight: Vec<usize>, capacity: usize) -> usize {
          let n = kids.len();
          let mut dp = vec![vec![0; capacity + 1]; n + 1];
        
          for i in 1..=n {
              for w in 1..=capacity {
                  if weight[i - 1] <= w {
                      dp[i][w] = dp[i - 1][w].max(kids[i - 1] + dp[i - 1][w - weight[i - 1]]);
                  } else {
                      dp[i][w] = dp[i - 1][w];
                  }
              }
          }
        
          dp[n][capacity]
      }
        
      #[http_component]
      fn handle_request(req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
          let (status, body) = match *req.method() {
              Method::POST => {
                  let request: KidsRequest =
                      serde_json::from_slice(req.body().clone().to_vec().as_slice()).unwrap();
        
                  let kids = request.kids;
                  let weight = request.weight;
                  let capacity = request.capacity;
        
                  let maximum_kids = optimal_kids_reached(kids, weight, capacity);
        
                  let response = KidsResponse { kids: maximum_kids };
        
                  let json_response = serde_json::to_string(&response)?;
        
                  (StatusCode::OK, json_response)
              }
              _ => (StatusCode::METHOD_NOT_ALLOWED, String::new()),
          };
          Ok(Response::builder()
              .status(status)
              .header("content-type", "application/json")
              .body(body)
              .build())
      }
    
  • spin build 로 빌드를 수행합니다.

    spin build

  • spin up 으로 로컬에서 실행이 가능합니다.

    spin up

  • curl을 사용하여 가볍게 테스트가 가능합니다.

      curl localhost:3000 -H 'Content-Type: application/json' -d '{"kids":[1,4,5,6,3,2,7], "weight":[23,45,23,43,12,32,45], "capacity":120}'
    

    curl

  • 아래 명령어로 제출 전 테스트가 가능합니다.

      hurl --test test.hurl
    

    test


2023 Advent-of-spin Challenge 1

이 포스트는 advent-of-spin에 업로드 된 Challenge 1에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • /index.html 에 Static page 호스팅 하기 (크리스마스 관련된 무언가를 담아서)
  • /data path에서 GET/POST method 구현하기
    • GET /data?advent : 저장된 value값 출력 / 성공시 200
    • POST: /data?advent body에 저장된 값 저장 / 성공시 201
         {
             "value": "<Matt의 자동화된 위시리스트>"
         }
      
    • response header에 Content-Type: application/json

Work

첫번째 요구사항인 /index.html 을 호스팅하기 위해 static-fileserver 을 사용합니다.

spin new static-fileserver

생성된 폴더 구조는 아래와 같습니다.

folder static-fileserver

assets 경로에 있는 파일들을 호스팅 해주는 파일서버 입니다.

두번째 요구사항를 위해 key-value store를 만들어줍니다.

spin new key-value

이 두 프로젝트를 별도의 서버가 아닌 하나의 서버로 작업할 것이기 때문에 두 프로젝트를 하나로 합칩니다.

합치고 난 이후에 이런 구조가 됩니다.

folder key-value

  • component는 key-value store인 spin-key-valueindex.html 을 호스팅해줄 fileserver 총 두가지 컴포넌트가 존재합니다.
  • /index.html 경로에 호스팅을 해 주어야 하기 때문에 fileserver 의 route 는 / 로 하였습니다.
  • 또한 index.html 만 사용하기 때문에 //index.html 로 변경하였습니다.
  • spin-key-value 의 경우 /data 만이 존재하기 때문에 route를 /data 로 하였습니다.
  • key_value_stores = ["default"]spin-key-value 컴포넌트에 추가하였습니다.

이를 실행해보면 다음과 같이 route가 생성됩니다.

route

이제 구현을 해야합니다.

assets/index.html 경로에 크리스마스 느낌의 이미지가 담긴 html 파일을 만들어줍니다.

<!DOCTYPE html>
<html lang="en">
    <body>
        <img src="https://www.shutterstock.com/image-vector/flork-meme-christmas-vector-ilustration-600nw-2175960549.jpg" />
    </body>
</html>

index.html

src/lib.rs 에 GET, POST를 처리하기 위한 로직을 구현합니다.

use http::{Method, StatusCode};
use spin_sdk::{
    http::{IntoResponse, Response},
    http_component,
    key_value::Store,
};

#[http_component]
fn handle_request(req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
    let store = Store::open_default()?;

    let (status, body) = match *req.method() {
        Method::POST => {
            store.set(req.uri().path(), req.body().as_slice())?;
            println!(
                "Storing value in the KV store with {:?} as the key",
                req.uri().path()
            );
            (StatusCode::CREATED, None)
        }
        Method::GET => match store.get(req.uri().path())? {
            Some(value) => {
                println!("Found value for the key {:?}", req.uri().path());
                (StatusCode::OK, Some(value))
            }
            None => {
                println!("No value found for the key {:?}", req.uri().path());
                (StatusCode::NOT_FOUND, None)
            }
        },
        Method::DELETE => {
            store.delete(req.uri().path())?;
            println!("Delete key {:?}", req.uri().path());
            (StatusCode::OK, None)
        }
        Method::HEAD => {
            let code = if store.exists(req.uri().path())? {
                println!("{:?} key found", req.uri().path());
                StatusCode::OK
            } else {
                println!("{:?} key not found", req.uri().path());
                StatusCode::NOT_FOUND
            };
            (code, None)
        }
        _ => (StatusCode::METHOD_NOT_ALLOWED, None),
    };
    Ok(Response::builder()
        .status(status)
        .header("content-type", "application/json")
        .body(body)
        .build())
}
  • spin build 로 빌드를 수행합니다.

    spin build

  • spin up 으로 로컬에서 실행이 가능합니다.

    spin up

  • curl을 사용하여 가볍게 테스트가 가능합니다.
    • POST

        curl -XPOST http://localhost:3000/data\?advent -H 'Content-Type: application/json' -d '{"value":"todolist
      

      POST

    • GET

        curl -XGET http://localhost:3000/data\?advent -H 'Content-Type: application/json' -d '{"value":"todolist"}'
      

      GET

  • 아래 명령어로 제출 전 테스트가 가능합니다.

      hurl --test test.hurl
    

    test


체인링크 CCIP란?

Chainlink CCIP

Chainlink Cross-Chain Interoperability Protocol (CCIP) 는 EVM 기반 체인에서 Cross-Chain Interoperability를 제공하는 프로토콜 입니다.

체인링크 CCIP는 아래와 같은 기능을 지원합니다.

  • Arbitrary Messaging: 임의의 데이터를 다른 네트워크의 Smart contract로 보낼 수 있습니다.

  • Token Transfer: 토큰을 다른 네트워크의 Smart contract 혹은 이더리움 주소로 전송할 수 있습니다.

  • Programmable Token Transfer: 단일 트랜잭션 내에서 임의 데이터와 토큰을 동시에 전송할 수 있습니다.

Architecture

architecture

위 사진은 CCIP의 기본적인 아키텍쳐를 보여줍니다.

Router는 Smart contract 형식으로 구현되어 있으며 아래와 같은 기능을 수행할 수 있습니다.

  • 다른 네트워크의 Smart contract transaction을 호출할 수 있습니다.
  • 다른 네트워크의 Smart contract 혹은 이더리움 주소로 토큰을 전송할 수 있습니다.
  • 동일한 트랜잭션으로 토큰과 임의의 메세지를 함께 보낼 수 있습니다.

Component

images

Onchain components

Router

Router는 기본 CCIP의 기본적인 smart contract이며 체인당 하나의 Router contract가 존재합니다.

Router contract는 명령을 OnRamp로 라우팅하는 역할을 수행합니다.

Commit store

Committing DONCommitStore contract와 상호작용하여 메시지의 Merkle root를 출발지 네트워크에 저장합니다.

Merkle root는 Risk Management Network 에 의해 블레싱(blessing) 되어야 Executing DON이 이를 목적지 네트워크에서 실행할 수 있습니다.

CommitStore은 메시지가 Risk Management Network에 의해서 blessing 되었고 레인(Lane)당 하나씩 존재하는지 체크합니다.

여기서 레인이란 출발지 네트워크와 목적지 네트워크간 경로를 의미합니다. 레인은 단발향이며 A->B 와 B->A는 별개의 레인입니다.

OnRamp

OnRamp contract는 레인당 하나씩 존재합니다. 이 컨트랙트는 아래와 같은 기능을 수행합니다.

  • 전송하려는 내용이 목적지 네트워크에 맞는지 검증합니다.

  • Message size limit, gas limits을 확인합니다.

  • 수신자의 메시지 순서를 보장하기 위해 메시지 순서를 관리합니다.

  • 수수료를 관리합니다.

  • 메시지에 토큰이 포함되어 있는경우 Token pools와 상호작용 합니다.

  • Committing DON이 모니터링 할 수 있도록 이벤트를 내보냅니다.

OffRamp

OffRamp contract는 레인당 하나씩 존재합니다. 이 컨트랙트는 아래와 같은 기능을 수행합니다.

  • Executing DON이 제공한 Proof를 사용하여 실제로 메시지가 커밋 되었고, Blessing 되었는지 확인합니다.

  • 트랜잭션이 정확하게 한번만 실행 되었는지 확인합니다.

  • 검증 이후 수신 된 모든 메시지를 Router로 전송합니다. 이 과정에서 토큰 전송이 포함된 경우 OffRamp는 TokenPool을 호출하여 토큰을 올바른 수신자에게 전송합니다.

Token pools

각각에 토큰은 OnRamp 혹은 OffRamp가 ERC-20에 대한 기능을 원활하게 하기 위한 token pool을 가지고 있습니다.

네이티브 토큰(eg. ETH, MATIC)의 경우엔 해당 기본 체인에서만 발행될 수 있기 때문에 Wrapped Token의 형식으로 목적지 네트워크에서 발행 됩니다.

총 발행량이 고정된 토큰에 경우 Lock and Mint의 방식을 사용하고

일반적으로 총 발행량이 고정되어 있지 않은 토큰에 경우 Burn and Mint의 방식을 사용합니다.

Risk Management Network contract

bless 혹은 curse에 대한 내역을 관리합니다.

OffChain components

Committing DON

Committing DON은 출발지 네트워크와 목적지 네트워크간 트랜잭션을 모니터링하는 작업을 수행합니다.

  • 출발지 네트워크에서 OnRamp contract의 이벤트를 모니터링 합니다.

  • 출발지 네트워크에서 블록이 finalize 되는것을 기다립니다.

  • 트랜잭션을 묶어서 Merkle root를 생성합니다.

  • 목적지 네트워크의 CommitStore contract에 Merkle root를 저장힙니다.

Executing DON

Executing DON은 출발지 네트워크와 목적지 네트워크간 트랜잭션을 실행하는 작업을 수행합니다.

  • 출발지 네트워크의 OnRamp contract의 이벤트를 모니터링 합니다.

  • 트랜잭션이 CommitStore contract에 존재하는 Merkle root의 일부인지 확인합니다.

  • Risk Management Network에 의해 메시지가 blessing 될 떄 까지 기다립니다.

  • Merkle root에 대하여 유효한 Merkle proof를 만들어 OffRamp contract를 통하여 트랜잭션을 실행합니다.

Risk Management Network

Risk Management Network는 Commitiing DON이 Commit Store에 커밋한 Merkle root를 모니터링하는 독립된 노드 집합입니다.

각 노드는 커밋된 Merkle root를 OnRamp contract에서 받은 트랜잭션과 비교합니다.

검증이 통과 되면 Risk Management Network contract를 이용하여 해당 Merkle root를 bless 합니다.

충분한 blessing이 있으면 해당 Merkle root를 실행할 수 있습니다.

만약 이상이 발생하는 경우 해당 시스템은 curse 합니다.


CRDT에 대해서

Overview

topology

CRDT (Conflict free Replicated Data Types)는 중앙 서버의 개입 없이, 유저 간 컨센서스, 동기화 과정을 거치지 않고도 동일한 상태를 가질 수 있도록 하는 기술입니다.

기존에는 OT (Operational Transformation) 방식을 사용했습니다. 중앙 서버가 유저들의 작업 내용을 모두 받아 이를 적절하게 보정하여 모든 유저들의 상태를 동일하게 하였습니다. 하지만 유저 간 통신으로 동기화가 불가능 하기 때문에 중앙 서버에 과부하가 올 수 있다는 문제점이 있었습니다.

CRDT (Conflict free Replicated Data Types)

CRDT

CRDT는 두 가지 방식이 존재합니다. State based CRDT(상태 기반 CRDT)와 Operation based CRDT(작업 기반 CRDT) 입니다.

Eventual Consistency

EC

CRDT를 다루기 전에 먼저 Eventual Consistency에 대한 설명이 필요합니다.

Eventual Consistency란 모든 복제본들에 대해서 새로운 상태 업데이트가 발생하지 않을때 동일한 값으로 수렴하는 것이 가능하다는 것입니다.

Eventual Consistency를 만족하기 위해서 아래 3가지 조건이 필요합니다

EC

  • Eventual Delivery: 어떤 복제본에 특정 method가 실행 되었다면 다른 복제본에서도 동일한 method가 실행 될 수 있습니다.

  • Convergence: 어떤 두 복제본이 동일한 method set을 가지고 수행되었다면 그 두 복제본은 같을 수 있습니다.

  • Termination: 모든 method는 정상적으로 실행 되어야 합니다.

EC limitation

Eventual Consistency를 만족하면 CRDT를 만족할 수 있느냐 라고 한다면 그렇지 않을 수 있습니다.

Eventual Consistency 만으로는 동일한 상태를 만들 수 있다는 가능성만을 나타낼 뿐 이를 확실할 순 없습니다.

Strong Eventual Consistency

그래서 조금 더 강력한 조건이 필요합니다. 이를 Strong Eventual Consistency라고 합니다.

SEC

Strong Eventual Consistency는 Eventual Consistency에서 업데이트가 다른 순서대로 오더라도 동일한 상태가 된다는 내용입니다.

SEC

이를 만족하기 위해서 Eventual Consistency를 만족하면서 아래 조건을 추가로 만족해야 합니다.

  • Strong Convergence: 어떤 두 복제본이 동일한 method set을 가지고 수행되었다면 그 두 복제본은 같은 상태를 가집니다.

그래서 CRDT를 달성한다는 것은 SEC를 만족한다는 것과 같은 말이라고 할 수 있습니다.

State based CRDT

State based CRDT

State based CRDT는 복제본 간에 상태를 전달하여 모든 복제본의 상태를 일치시키는 방법입니다.

State based CRDT에서 각 복제본은 아래의 튜플을 구성합니다.

$(S, S^0, q, u, m)$

$ S = State, S^0 = Initial \ State$

$ q=Query, u=Update, m=Merge $

Merge function

State의 순서에 상관없이 모든 복제본이 동일한 상태로 수렴하게 하려면 Merge function이 매우 중요합니다.

그러기 위해서 Merge function은 아래의 조건을 만족해야 합니다.

merge function

  • 교환법칙: 두 상태를 결합할 때 순서를 바꾸더라도 결과 값은 동일하다

  • 멱등법칙: 동일한 두 상태를 결합할 때 결합한 상태와 같은 값이 나와야 한다.

  • 결합법칙: 3개 이상의 상태에 대해서 결합을 하고자 할 때, 어떤 2개의 상태를 먼저 결합을 하더라도 결과 값은 동일하다.

위의 3가지 조건을 만족하는 Merge function을 구성한다면 State based CRDT를 만족할 수 있게 됩니다.

Operation based CRDT

Operation based CRDT

Operation based CRDT는 복제본 간에 수행된 작업을 전달하여 모든 복제본의 상태를 일치시키는 방법입니다.

State based CRDT에서 각 복제본은 아래의 튜플을 구성합니다.

$(S, S^0, q, u, t m)$

$ S = State, S^0 = Initial \ State$

$ q=Query, t= prepare-update, u=effect-update, m=Merge $

State based CRDT에서 있던 Merge 가 사라지고 Update가 $(u,t)$ 쌍으로 구분 되었습니다.

Commutativity

Commutativity1

Operation based CRDT에서도 State based CRDT와 마찬가지로 순서에 상관없이 동일한 상태로 수렴할 수 있어야 합니다.

그러기 위해서 Update 연산에 대해서 순서를 바꾸더라도 동일한 결과값을 획득할 수 있어야 합니다.

Commutativity2

위 사진에 보이는것 처럼 $\alpha$, $\beta$ 업데이트를 한다고 했을 때 순서를 바꿔서 업데이트를 수행하더라도 동일한 결과를 얻을 수 있어야 합니다.

이를 만족하는 Update method를 구성한다면 Operation based CRDT를 만족할 수 있게 됩니다.

Sidetree?

이전 포스팅에서 Sidetree protocol에 대해서 다루었는데 Sidetree protocol에서 Sidetree node간 상태를 일치시키는데 CRDT를 사용하고 있습니다.

그러면 Sidetree에서는 State based CRDT를 사용할까요? Operation based CRDT를 사용할까요?

Delta based CRDT

State based CRDT는 모든 복제본에 상태값을 전달하게 되는데 상태값 전체를 전달하게 되면 전송하는 데이터의 사이즈가 굉장히 커질 수 있습니다.

more CRDT

이를 보완하고자 변경 된 상태에 대해서만 다른 복제본으로 전달하는 Delta based CRDT가 파생 되었습니다.

Delta based CRDT

각 복제본은 전체 상태를 다른 복제본으로 전달하지 않고 변경된 상태에 대해서만 다른 복제본으로 전달하게 되고 그 이후 과정은 State based CRDT와 동일하게 수행됩니다.