Giter Club home page Giter Club logo

starknet_in_rust's Introduction

πŸ¦€ Starknet in Rust πŸ¦€

Starknet transaction execution library in Rust, featuring ⚑cairo-vm⚑

Report Bug Β· Request Feature

codecov license pr-welcome Telegram Chat

Table of Contents

⚠️ Disclaimer

🚧 This project is a work-in-progress and is not ready for production yet. Use at your own risk. 🚧

πŸ“– About

starknet_in_rust is an implementation of Starknet in Rust. It makes use of cairo-vm, the Rust implementation of the Cairo virtual machine.

πŸŒ… Getting Started

Dependencies

  • Rust 1.74.1
  • A working installation of cairo-lang 0.12 (for compiling the cairo files)
  • [Optional, for testing purposes] Heaptrack

Requirements

You need to have a version of Python 3 installed. If you don't have it, you can install it for Debian-based GNU/Linux distributions with:

sudo apt install python3.9

On MacOS you can use Homebrew:

brew install [email protected]

Optionally, for setting environment, you can install pyenv for MacOS:

brew install pyenv

Installation

If you run make on it's own it will print out the main targets and their description.

Run the following make targets to have a working environment (if in Mac or if you encounter an error, see the subsection below):

Linux (x86-64)

$ make deps
$ make build

OSX (Apple Silicon)

$ make deps-macos
$ make build

Check the Makefile for additional targets.

How to manually install the script dependencies

cairo-lang requires the gmp library to build. You can install it on Debian-based GNU/Linux distributions with:

sudo apt install -y libgmp3-dev

In Mac you can use Homebrew:

brew install gmp

In Mac you'll also need to tell the script where to find the gmp lib:

export CFLAGS=-I/opt/homebrew/opt/gmp/include LDFLAGS=-L/opt/homebrew/opt/gmp/lib

Cairo Native support

Starknet in Rust can be integrated with Cairo Native, which makes the execution of sierra programs possible through native machine code. To use it, the following needs to be setup:

  • LLVM 17 needs to be installed and the MLIR_SYS_170_PREFIX and TABLEGEN_170_PREFIX environment variable needs to point to said installation. In macOS, run
    brew install llvm@17
    export MLIR_SYS_170_PREFIX=/opt/homebrew/opt/llvm@17
    export LLVM_SYS_170_PREFIX=/opt/homebrew/opt/llvm@17
    export TABLEGEN_170_PREFIX=/opt/homebrew/opt/llvm@17
    
    and you're set.

Afterwards, compiling with the feature flag cairo-native will enable native execution. You can check out some example test code that uses it under tests/cairo_native.rs.

Using ahead of time compilation with Native.

Currently cairo-native with AOT needs a runtime library in a known place. For this you need to compile the cairo-native-runtime crate and point the following environment variable to a folder containing the dynamic library. The path must be an absolute path.

CAIRO_NATIVE_RUNTIME_LIBDIR=/absolute/path/to/cairo-native/target/release

If you don't do this you will get a linker error when using AOT.

πŸš€ Usage

Running simple contracts

You can find a tutorial on running contracts here.

Customization

Contract class cache behavior

starknet_in_rust supports caching contracts in memory. Caching the contracts is useful for avoiding excessive RPC API usage and keeping the contract class deserialization overhead to the minimum. The project provides two builtin cache policies: null and permanent. The null cache behaves as if there was no cache at all. The permanent cache caches everything in memory forever.

In addition to those two, an example is provided that implements and uses an LRU cache policy. Long-running applications should ideally implement a cache algorithm suited to their needs or alternatively use our example's implementation to avoid spamming the API when using the null cache or blowing the memory usage when running with the permanent cache.

Customized cache policies may be used by implementing the ContractClassCache trait. Check out our LRU cache example for more details. Updating the cache requires manually merging the local state cache into the shared cache manually. This can be done by calling the drain_private_contract_class_cache on the CachedState instance.

// To use the null cache (aka. no cache at all), create the state as follows:
let cache = Arc::new(NullContractClassCache::default());
let state1 = CachedState::new(state_reader.clone(), cache.clone());
let state2 = CachedState::new(state_reader.clone(), cache.clone()); // Cache is reused.

// Insert state usage here.

// The null cache doesn't have any method to extend it since it has no data.
// If the permanent cache is preferred, then use `PermanentContractClassCache` instead:
let cache = Arc::new(PermanentContractClassCache::default());
let state1 = CachedState::new(state_reader.clone(), cache.clone());
let state2 = CachedState::new(state_reader.clone(), cache.clone()); // Cache is reused.

// Insert state usage here.

// Extend the shared cache with the states' contracts after using them.
cache.extend(state1.state.drain_private_contract_class_cache());
cache.extend(state2.state.drain_private_contract_class_cache());

Logging configuration

This project uses the tracing crate as a library. Check out its documentation for more information.

Testing

Logging configuration

This project uses the tracing crate as a library. Check out its documentation for more information.

Testing

Run the following command:

$ make test

Take into account that some tests use the RPC State Reader so you need a full-node instance or an RPC provider that supports Starknet API version 0.6.0.

RPC State Reader

The RPC State Reader provides a way of reading the real Starknet State when using Starknet in Rust. So you can re-execute an existing transaction in any of the Starknet networks in an easy way, just providing the transaction hash, the block number and the network in which the transaction was executed. Every time it needs to read a storage value, a contract class or contract, it goes to an RPC to fetch them.

Right now we are using it for internal testing but we plan to release it as a library soon.

How to configure it

In order to use the RPC state reader add the endpoints to a full node instance or RPC provider supporting Starknet API version 0.5.0 in a .env file at root:

RPC_ENDPOINT_TESTNET={some endpoint}
RPC_ENDPOINT_MAINNET={some endpoint}

Profiling

Run the following command:

$ make flamegraph

to generate a flamegraph with info of the execution of the main operations.

Benchmarking

Read the 'bench_integration.py' file to identify which lines need to be commented out for accurate results. Comment out those lines and then run the following command:

$ make benchmark

πŸ›  Contributing

The open source community is a fantastic place for learning, inspiration, and creation, and this is all thanks to contributions from people like you. Your contributions are greatly appreciated.

If you have any suggestions for how to improve the project, please feel free to fork the repo and create a pull request, or open an issue with the tag 'enhancement'.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

And don't forget to give the project a star! ⭐ Thank you again for your support.

🌞 Related Projects

  • cairo-vm: A fast implementation of the Cairo VM in Rust.
  • cairo-vm-py: Bindings for using cairo-vm from Python code.

πŸ“š Documentation

Starknet

βš–οΈ License

This project is licensed under the Apache 2.0 license.

See LICENSE for more information.

starknet_in_rust's People

Contributors

aminarria avatar azteca1998 avatar edg-l avatar elfantasma avatar entropidelic avatar fguthmann avatar fmoletta avatar hermanobst avatar igamigo avatar igaray avatar jordibonet-lambdaclass avatar juan-m-v avatar juanbono avatar kkovaacs avatar klaus993 avatar marco-paulucci avatar marioiordanov avatar martiangreed avatar martinacantaro avatar matias-gonz avatar megaredhand avatar mfachal avatar mmsc2 avatar omahs avatar oppen avatar pefontana avatar rcatalan98 avatar santiagopittella avatar toni-calvin avatar xqft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

starknet_in_rust's Issues

Refactor for InMemoryStateReader and add test

The struct InMemoryStateReader is used for testing. It contains everything related to the Starknet state, but instead of interacting with a full node, it has everything in memory.

This task can be divided into the following steps:

1- Create a new type called ClassHash in utils.rs module

pub type ClassHash = [u8; 32]

2- Create a new type called ContractStorageKey in state-api

pub type ContractStorageKey = (Address, Felt)

3- Remove the existent fields in InMemoryStateReader and replace it for:

pub contract_storage: HashMap<ContractStorageKey, Felt>,
pub address_to_nonce: HashMap<Address, Felt>,
pub address_to_class_hash: HashMap<Address, ClassHash>,
pub class_hash_to_class: HashMap<ClassHash, ContractClass>

4- Remove get_contract_state

5- Fix new method and the StateReader implementation.

6- Fix and add tests for the functionality implemented

create a trait for Transactions

there is repeated behavior in most of the transactions, for example the calls to charge_fee(), apply() or execute(), this functionality could be encapsulated inside a trait an then implemented for each internal transaction.

Fix fee calculation

Task was halted as we found out that there are currently no tests related to fees in blockifier

Test trasfer amount errors

Add integration tests:

  • transfering incorrect amounts (negative, higher than available,etc).
  • Minting fee to an account
  • Making transactions without fee
  • Checking fee calculation

Implement code coverage check in CI

The goal is to have a worklflow similar to the one in the cairo-rs repo

The workflow checks the total coverage and the coverage of the changes made by the PR need to be over a certain threshold or else the workflow fails.
You can see an example in this PR (check the codecov/patch and codecov/project checks)
lambdaclass/cairo-vm#910

This is done by an app linked to the repository. Research how to replicate this.

Refactor State trait: Remove block_info

The State trait is the functionality abstraction for the Starknet State. It provides methods for writing and reading the State.
We need to remove the method: fn block_info(&self) -> &BlockInfo

This task can be divided into:
1- Remove block_info from trait State
2- Remove the methods from the Structs that implements the trait.

Add missing functions in State API

From Ariel's message:

setters

  • update_block_context(block_context)

  • add_class(class_hash, class)

    • Note again that before regenesis, this class can be two different creatures (Casm or Sierra).
  • set_class_at(contract_address, class_hash) - used in the new replace_class system call

  • deploy_contract(contract_address, class_hash)

  • set_storage_at(contract_address, key, value)

  • increment_nonce(contract_address) - in the future this may be generalized further (we want to add mechanism that allow accounts with no sequential nonce)

apply_state_updates(state_update)

Adapt execute_entrypoint to execute the new contract classes

Depends on #313

We need to update our execute_entrypoint module to take into account the new contract. classes from cairo 1. In cairo-lang 0.11 this is implemented in the execute_entrypoint.py module using an IF statement and switching to the relevant implementation based on the type of the contractClass being executed. We should do something similar and implement the execute_version0 and the new execute methods.

Fix functionality in _storage_read

In order to not returning an error while trying to read a declared (but not written) @storage_var in an starknet Contract, we are matching errors and returning a zero in _storage_read syscall

This is not correct. It would be better to initialize that storage var with value zero in the state

Cargo docs are not building

The command cargo doc outputs this:

error: unresolved link to `x,y,z`
  --> src/core/transaction_hash/starknet_transaction_hash.rs:49:8
   |
49 | /// H([x,y,z]) = h(h(x,y),z) = H([w, z]) where w = h(x,y).
   |        ^^^^^ no item named `x,y,z` in scope
   |
   = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
note: the lint level is defined here
  --> src/lib.rs:1:9
   |
1  | #![deny(warnings)]
   |         ^^^^^^^^
   = note: `#[deny(rustdoc::broken_intra_doc_links)]` implied by `#[deny(warnings)]`

error: could not document `starknet-rs`

Caused by:
  process didn't exit successfully: `rustdoc --edition=2021 --crate-type lib --crate-name starknet_rs src/lib.rs -o /Users/martina/Lambda/starknet_in_rust/target/doc --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat -C metadata=3a799cd784b699f8 -L dependency=/Users/martina/Lambda/starknet_in_rust/target/debug/deps --extern felt=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libcairo_felt-10095009f4cbdd1e.rmeta --extern cairo_rs=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libcairo_vm-5288c247c46d4c0b.rmeta --extern getset=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libgetset-a37ed115cd8c00c6.dylib --extern lazy_static=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/liblazy_static-efab0965e7c94ec0.rmeta --extern num_bigint=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libnum_bigint-ba064e1bb14317c0.rmeta --extern num_integer=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libnum_integer-4ad9686def0b596b.rmeta --extern num_traits=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libnum_traits-b63daa3697186bbd.rmeta --extern serde=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libserde-b1ee38ec87500af2.rmeta --extern serde_json=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libserde_json-e5e8f9da9c7c78d2.rmeta --extern sha3=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libsha3-eecc83fa9da46664.rmeta --extern starknet_crypto=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libstarknet_crypto-87fc6743e0e7a4f2.rmeta --extern starknet_api=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libstarknet_api-cc81798387834a4d.rmeta --extern thiserror=/Users/martina/Lambda/starknet_in_rust/target/debug/deps/libthiserror-5c6fa917eba97cfc.rmeta --crate-version 0.1.0` (exit status: 1)

Test ContractClass::validate

The validation just checks that the program's prime is the same as the Cairo prime and checks that the program's included builtins are supported and in the correct order.

Included builtins (ordered):

"pedersen"
"range_check"
"output"
"ecdsa"
"bitwise"
"ec_op"
"keccak"

Please take into account that the builtins are ordered.

Use perfect hashing instead of string matching in SyscallHintProcessor::execute_syscall_hint

The SyscallHintProcessor is the component that is in charge of executing what is called Syscall Hints. They are a special kind of hint that are only used in the context of StarkNet since they interact with the StarkNet State.

In order to execute a hint we first match the hint code and then we dispatch to the implementation of that hint. This is a linear process because we try every hint code until it matches one or fails.

match &*hint_data.code {
DEPLOY => Err(SyscallHandlerError::NotImplemented),
EMIT_EVENT_CODE => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.emit_event(vm, syscall_ptr)
}
GET_BLOCK_NUMBER => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_block_number(vm, syscall_ptr)
}
GET_BLOCK_TIMESTAMP => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_block_timestamp(vm, syscall_ptr)
}
GET_CALLER_ADDRESS => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_caller_address(vm, syscall_ptr)
}
GET_SEQUENCER_ADDRESS => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_sequencer_address(vm, syscall_ptr)
}
LIBRARY_CALL => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.library_call(vm, syscall_ptr)
}
STORAGE_READ => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.storage_read(vm, syscall_ptr)
}
STORAGE_WRITE => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.storage_write(vm, syscall_ptr)
}
SEND_MESSAGE_TO_L1 => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.send_message_to_l1(vm, syscall_ptr)
}
GET_TX_SIGNATURE => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_tx_signature(vm, syscall_ptr)
}
GET_TX_INFO => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_tx_info(vm, syscall_ptr)
}
GET_CONTRACT_ADDRESS => {
let syscall_ptr = get_syscall_ptr(vm, &hint_data.ids_data, &hint_data.ap_tracking)?;
self.syscall_handler.get_contract_address(vm, syscall_ptr)

To improve the performance of this section of the code we can use perfect hashing at compile time.

We suggest using this library for implementing it.

Remove useless structs

We have to remove all this structs and types that are no longer needed.

ATTENTION: We have to think if we need to remove this module entirely. Also the Storage trait maybe not needed.

pub type StorageKey = (Prefix, [u8; 32]);

pub(crate) struct DictStorage {
storage: HashMap<StorageKey, Vec>,
}

impl DictStorage {
pub fn new() -> Self {
DictStorage {
storage: HashMap::new(),
}
}
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)]
pub enum Prefix {
Int,
Float,
Str,
ContractState,
ContractClass,
}

Remove _ functions from the SyscallHandler trait

In the SyscallHandler trait implementation, we have functions with similar names like deploy and _deploy or _get_caller_address and get_caller_address, which cause some confusion.
In all these cases, the _ functions have a different implementation for each OsSyscallHandler and BusinessLogicSyscallHandler, while the functions without _ have the same implementation for both.
The idea of this issue is to remove these _ functions, to avoid confusion.

One possible solution could be to implement a SyscallWrapper struct.

pub struct SyscallWrapper<T>(T)
where
    T: SyscallHandler;

The SyscallHandler trait takes care of these _ functions that differ for each syscall. And the normal functions (the ones that have the same implementation for each syscall) can be implemented as SyscallWrapper methods.

Installation: ensure the CFLAGs and LDFLAGs are being used in the Makefile

Describe the bug
Currently, when building the repo in MacOS, users have to manually add the CFLAGs and LDFLAGs. This should be done by the Makefile but it's broken.

To Reproduce
Steps to reproduce the behavior:
Do a clean installation of the repo on MacOS. The make deps command will fail with the following message (I abbreviated it to the important part):

      src/curve.h:4:10: fatal error: 'gmp.h' file not found
      #include "gmp.h"
               ^~~~~~~
      1 error generated.
      /private/var/folders/hg/k0r8kt7j4jv7r_mgwzz0ks500000gn/T/pip-build-env-o4oob_qk/overlay/site-packages/setuptools/config/pyprojecttoml.py:108: _BetaConfiguration: Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*.
        warnings.warn(msg, _BetaConfiguration)
      error: command '/usr/bin/gcc' failed with exit code 1
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for fastecdsa
Failed to build fastecdsa
ERROR: Could not build wheels for fastecdsa, which is required to install pyproject.toml-based projects

Expected behavior
Running make deps should not break

Additional context
We temporarily fixed this by giving users a command that fixes this in the readme, but this manual step should be replaced by a make deps that actually works on Mac.

Delete unused module

The state.rs module contains lots of warning inhibitors due to dead code.

I believe everything except for the ExecutionResourcesManager struct is dead code that was ported over from the Python version two months ago in case it was ever needed.

Goal: delete the dead code. If we indeed need this in the future, we will create it again.

Use generics State in InternalInvokeFunction

In the InternalInvokeFunction methods:

  • run_validate_entrypoint
  • run_execute_entrypoint
  • _apply_specific_concurrent_changes
    replace the state: &mut CachedState<InMemoryStateReader> for a generic that implements the State + StateReader traits.

To resolve this issue we first need to resolve #128

Implement a StateReader that reads from a full-node

The StateReader is a trait that defines the API for reading from the Starknet State.

pub trait StateReader {
/// Returns the contract class of the given class hash.
fn get_contract_class(&mut self, class_hash: &[u8; 32]) -> Result<ContractClass, StateError>;
/// Returns the class hash of the contract class at the given address.
fn get_class_hash_at(&mut self, contract_address: &Address) -> Result<&[u8; 32], StateError>;
/// Returns the nonce of the given contract instance.
fn get_nonce_at(&mut self, contract_address: &Address) -> Result<&Felt, StateError>;
/// Returns the storage value under the given key in the given contract instance.
fn get_storage_at(&mut self, storage_entry: &StorageEntry) -> Result<&Felt, StateError>;

Right now, we have implemented an in-memory state reader that reads from a hashmap.

The goal of this task is to implement a StateReader that reads from a full-node implementation like Papyrus and Pathfinder

Implement the new BusinessLogicSyscallHandler

Cairo-lang 0.11 introduced a new implementation of the BusinessLogicSyscallHandler and now there are 2 implementations at the same time. One used for 0.x execution and the new one for cairo 1 contract execution.

Note: You should not replace or modify the current implementation. Just add the new one.

Replace InMemoryStateReader for a generic type.

Currently we are using InMemoryStateReader struct to test the flow of the system, in the future we have to replace it with the a generic type T that implements StateReader and State traits.

examples where it should be changed:

  • execute() in execute_entry_point
  • run() in execute_entry_point
  • impl block for SyscallHintProcessor<BusinessLogicSyscallHandler<>>
  • struct StarknetRunner in the field hint_processor and its impl block

Remove UpdatesTrackerState module

  • Move the UpdatesTrackerState.set_storage_at logic to CachedState.set_storage_at
  • Idem with get_storage_at
  • Remove the UpdatesTrackerState module

Add integration test for replace_class syscall

It should execute a cairo 1 contract that makes a call to replace_class and it should check the state to validate that the class_hash has changed. We can use the example in the starknet docs changelog as a test case for this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.