2023 Advent-of-spin Challenge 4

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

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

Spec

  • Bulls’n’Cows 게임을 진행합니다. 숫자야구와 동일한 게임입니다.
  • https://bulls-n-cows.fermyon.app/api API를 통해서 게임을 진행할 수 있으며 이 게임을 진행할 수 있는 로직을 구현합니다.
  • https://bulls-n-cows.fermyon.app/api?guess=012 식으로 query에 guess 를 통해서 값을 전달할 수 있으며 응답은 아래와 같습니다.
  • 이후 동일한 게임을 이어서 할 때 • https://bulls-n-cows.fermyon.app/api?guess=012&id=1836633b-8dd1-41c2-b084-644b37c6fd1b 처럼 id를 같이 전달합니다.

Work

  • 이번 챌린지는 로직 구현에 초점을 맞추어 진행합니다. 라우터는 GET 하나만 뚫어서 진행합니다.
  • spin new 명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.

    spin new

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

    dependencies

    • request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
  • src/lib.rs 에 POST를 처리하기 위한 로직을 구현합니다.

    • 경우의 수가 많지 않아(0,1,2,3,4 만 사용) 모든 경우를 array에 넣고 모든 경우를 시도하는 방법으로 구현하였습니다.
      use http::StatusCode;
      use serde::{Deserialize, Serialize};
      use spin_sdk::{
          http::{IntoResponse, Method, Request, Response},
          http_component,
      };
    
      #[derive(Debug, Deserialize, Serialize)]
      struct GameResponse {
          cows: u64,
          bulls: u64,
          gameId: String,
          guesses: u64,
          solved: bool,
      }
    
      #[http_component]
      async fn handle_request(_req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
          let guess_numbers = [
              "013", "014", "021", "023", "024", "031", "032", "034", "102", "103", "104", "120",
              "123", "124", "130", "132", "134", "201", "203", "204", "210", "213", "214", "230", "231",
              "234", "301", "302", "304", "310", "312", "314", "320", "321", "324",
          ];
    
          let status = StatusCode::OK;
          let body = "Done".to_string();
    
          let uri = format!("https://bulls-n-cows.fermyon.app/api?guess=012");
    
          let req = Request::builder().method(Method::Get).uri(uri).build();
    
          // Send the request and await the response
          let res: Response = spin_sdk::http::send(req).await?;
    
          let response: GameResponse = serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();     
    
          if response.solved {
              println!("{:?}", response);
              Ok(Response::builder()
              .status(status)
              .header("content-type", "application/json")
              .body(body)
              .build())
          }
          else {
              let game_id = response.gameId;
              for guess in guess_numbers.iter() {
                  let uri = format!("https://bulls-n-cows.fermyon.app/api?guess={}&id={}", guess, game_id);
                
                  let req = Request::builder().method(Method::Get).uri(uri).build();
                
                  // Send the request and await the response
                  let res: Response = spin_sdk::http::send(req).await?;
                
                  let response: GameResponse = serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();     
            
                  if response.solved {
                      println!("{:?}", response);
                      break;
                  }
              }
              Ok(Response::builder()
                  .status(status)
                  .header("content-type", "application/json")
                  .body(body)
                  .build())
            
              }
          }
    

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 합니다.