Democratizing Access to Utilities — TUTORIAL: Building a Blockchain-Controlled Smart Lock

Julien Carbonnell
Partage
Published in
13 min readOct 9, 2023

Following my research on Blockchain for Smart-Cities I came to the conclusion that building a Blockchain-Controlled Smart Lock was a great solution to increase the use value of cities and experiment with the democratization of access to utilities in urban areas.

At the same time, I was looking for a use case to extend the interoperability of Partage — a Shared NFT Utilities platform that I deployed on the Stacks Blockchain earlier this year. Partage is a fractional ownership protocol for Utility NFTs and a marketplace to trade shares into Utilities. I found that the Encode Club was providing a convenient roadmap of some months with the Near Blockchain, starting with a boot camp in July and August, a hackathon in September, and a conference in November, followed by a startup accelerator program. I decided to follow this track and build my Smart Lock on Near with an Arduino Uno Rev3.

Plan:

  1. Building the prototype
    - Hardware
    - Software
    - Blockchain as a Database
    - Smart Contract
    - Frontend
  2. Next Steps

1. Prototyping

I was staying in Montreal for the month of September when I started to work on the hardware side of this project. I visited a couple of FabLabs to get the tools and support I needed. The first one is the PolyFab, the FabLab of the Polytechnique School of Montreal, which gave me access to their laboratory, and the second one is the FabLab du PEC, which helped me by sharing their experience in building such a project.

As usual, I started by reviewing a bunch of tutorials covering my topic to find my way and finally narrow down my final solution. This process comes with a variable amount of mistakes which decreases over time while I learn about the topic targetted. My first struggle came from my Arduino model choice. If most tutorials were building the solenoid door lock on Arduino Uno and Nano, I first chose a Nano 33 IoT which seemed to be best adapted to home automation projects, with convenient built-in wifi, and Bluetooth features. However, I underestimated the compatibility issues that I found when I started to build on it: the Nano 33 IoT has a specific processor SAMD1 which requests specific libraries instead of the standard libraries used with the standard Arduino boards built with an ATMega328 microcontroller. In addition to the need for specific software modules, the Arduino Nano 33 IoT runs on 3.3V instead of regular 5V, which makes the other hardware components of the system incompatible (namely: the I2C LED screen and the relay module). As a consequence, after some days of struggle, I swapped my Arduino Nano 33 IoT for a standard Arduino Uno and started over. Below is the list of the current hardware I’m using.

Hardware:

My prototype uses an Arduino UNO microcontroller, which acts as the brain of the lock, and a relay, which is responsible for switching the power supply to the DC solenoid.

Pin map of the Arduino Uno Rev 3

A 5V SPDT (Single Pole Double Throw) relay-controlled DC solenoid lock is an electronic device that allows the control of the locking of a door electronically. The relay is controlled by the Arduino UNO microcontroller, which can be programmed to lock and unlock the door at specific times or in response to certain events. A 12V DC solenoid is an electromechanical device that converts electrical energy into linear motion. It consists of a coil of wire that generates a magnetic field when an electric current flows through it and a plunger or armature that is attracted to the magnetic field. When the current is removed, a spring returns the plunger to its original position.

Wiring map of the components

Software:

You will need to use the Arduino IDE to write your code and upload it via USB to your Arduino board. You can either download the Arduino IDE locally or use its online version. For the below code to compile successfully, you will also need to install a few libraries from the library manager on your IDE. So far, you’ll need the LiquidCrystal_I2C library to support your LCD screen, and the Keypad library to support the keypad. Later on, you’ll also need the ESP8266 library to support the Wi-Fi module, the NTPClient library to get the date and time from an NTP server, and the WiFiUDP library to send and receive UDP messages.

The code on Arduino is usually structured like this:
- libraries import
- define constants
- a setup function
- a loop function.

The “setup” function is executed once when you power on the Arduino or press the reset button. It initializes the digital pin A5 as an output, which is connected to the IN pin of the relay. This pin will be used to control the relay and lock/unlock the door.

The “loop” function runs repeatedly and is responsible for controlling the relay and locking and unlocking the door. In this code, the relay is locked by default (position set to LOW) and by setting the digital pin A5 to HIGH, the Arduino activates the relay and unlocks the door. This state is maintained for 5 seconds using the delay() function. Then the relay is unlocked by setting the digital pin A5 to LOW again, which deactivates the relay and locks the door back.

// import libraries
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

// constants won't change
// pin connected to the lock relay
const int RELAY_PIN = A0;

// change password length below
const int Password_Length = 4;

// Character to hold password input
String Data;
// static password, will be muted once connected to the blockchain
String password = "1234";
// Character to hold key input
char customKey;

// Constants for keypad rows and columns sizes
const byte ROWS = 4;
const byte COLS = 4;

// mapping the keys of the keypad
char keys[ROWS][COLS] =
{
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
//connection of rows pins to the Arduino
byte rowPins[ROWS] = { 13, 12, 11, 10 };
// connection of the columns pins to the Arduino
byte colPins[COLS] = { 9, 8, 7, 6 };
// Counter for character entries
byte data_count = 0;

// create a keypad object
Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
// create LCD object
LiquidCrystal_I2C lcd(0x27, 16, 2); // I2C address 0x27, 16 column and 2 rows

// the setup function runs once when you press reset or power the board
void setup() {
// initialize the lcd
lcd.init();
lcd.backlight();
// initialize digital pin 3 as an output.
pinMode(RELAY_PIN, OUTPUT);
// print a welcoming message
lcd.clear(); // clear display
lcd.setCursor(0, 0); // move cursor to (0, 0)
lcd.print("Partage Lock"); // print message at (0, 0)
lcd.setCursor(2, 1); // move cursor to (2, 1)
lcd.print("hellopartage.xyz"); // print message at (2, 1)
delay(5000); // display the above for five seconds
}

// the loop function runs over and over again forever
void loop() {
// pending password message on lcd
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enter Password:");

// collect input from keypad
char key = keypad.getKey();
if (key) {
// Enter input keys into array and increment counter
Data += key;
lcd.setCursor(data_count, 1);
lcd.print(Data[data_count]);
data_count++;
}
// See if we have reached the password length
if (data_count == Password_Length) {
lcd.clear();

// if correct password entered
if (Data == password) {
// print correct
lcd.print("Correct");
// turn on the relay for 5 seconds
digitalWrite(RELAY_PIN, HIGH); // unlock the door
delay(5000); // wait 5 sec
digitalWrite(RELAY_PIN, LOW); // lock the door
}
// if incorrect password entered
else {
// print incorrect
lcd.print("Incorrect");
delay(1000);
}

// clear data and LCD display
lcd.clear();
clearData();
}
}

void clearData() {
//Reset data_count
data_count = 0;
//Reset Data
Data ="";
}

Blockchain as a Database:

In the above version, the password is determined statically in the code, currently set to “1234”. What we want is a dynamic password, which will be different each time a user buys access to the locked utility. Therefore, we need to store a series of secret passwords attached to calendar dates in a database. On the one hand, when a user buys access to a shared utility for a specific day on the calendar, a random 4-digit password will be generated, displayed on the user’s web page, hashed, and the hash will be published on-chain. On the other hand, when a user enters a password on the keypad of the lock, the Arduino will get the date from the internet by using the Network Time Protocol (NTP), fetch the hashed password stored on-chain, and compare it with the entered password. If the user enters the correct password, the lock will open, if the password entered does not match the password hashed on-chain, the lock will remain closed. The blockchain here acts like a regular database when a user types a password to retrieve it’s profile on any website.

For this, I first need to add a WiFi module to my system and fetch the blockchain data from my Arduino. Later on, we will also need to get the date and time, so that multiple users will use different passwords to access a shared utility at different calendar dates. Adding a real-time clock module to the system would be an option, but those work on battery (needs maintenance), and since the lock will connect to the internet to fetch the password data from the blockchain, it can also handle retrieving the current date and time from the NTP.

When I added the Wi-Fi module ESP8266 ESP-01 to my hardware assembly, updated my code accordingly, and tried to upload it into my Arduino, I first bumped into an error saying “A fatal esptool.py error occurred: Failed to connect to ESP8266: Timed out waiting for packet header”. After some research, I found that the most common explanation for this is that the ESP8266 module can’t rely directly on the 3.3V alim from Arduino and needs a third-party adapter to successfully connect to wifi.

The WiFi module allows the Arduino to connect to a wireless internet.

Smart Contract:

I first thought of handling most of the app functionality from the smart contract. I imagined that it could handle the payment, generate a random 4-digit password, hash it, send it to the user’s email, and store it in the booking’s attached data on-chain. If such functionality was running fine locally using common Rust crates, I realized after some struggles that the Near-SDK, a Rust library used for writing NEAR smart contracts, doesn’t allow all Rust crates, for the simple reason that some functionalities would be too heavy to be run on-chain. Therefore I had to simplify my smart contract as much as possible until I had one smart contract successfully responsive on the Near Blockchain’s test net.

In this last version, the blockchain will be used to transfer a direct payment between the utility provider and the user. The Add Booking function that can be called publicly by any Near user will do the following: it will receive a payment in Near, transfer it from the buyer to the utility owner, fetch and hash a random 4-digit number displayed on the user’s interface from the frontend, and store that hash in the booking’s on-chain data. While this hash is a first level of security, it is not enough to protect against all malicious users who could potentially find ways to retrieve the original password from the public hash. Let's say it’s good enough for now, as we are building a working prototype to present a proof of concept to potential partners. I will work on adding more security to the system if I can reach interest from the blockchain community.

The app is built with a nextJs framework, adapted to the Near blockchain, meaning it is mostly composed of a backend contract written in Rust, and a frontend written in Javascript. The code of the Partage Lock is openly shared on GitHub https://github.com/PartageProtocol/partage-lock, so you can clone it on your local machine and start filling down your elements to connect your Partage Lock to the Near blockchain. Together we will go through the key components of the smart contract, and go through the main steps for you to deploy your own shared utilities in the streets of your city.

  • Set up your Near Testnet Wallet
    The first thing you need is to create a wallet or account on the Near test net. You will receive test tokens at the creation and will use them to simulate transactions from your wallet to the smart contract. For this the simplest way is https://testnet.mynearwallet.com/
  • Set up your environment
    Install the NEAR Command Line Interface and log in your wallet to your session using the near login command. Once approved, you will see a screen indicating that your login was successful.
  • Configure Rust to compile your smart contract
    In your terminal, enter the following command to be able to compile our contracts correctly in the WASM target for deployment.
rustup target add wasm32-unknown-unknown

Now let’s take a look at the actual repository

  • The “utils.rs” file is designed to contain some core variables, constants, and functions for various unit conversions.
use near_sdk::{
env,
PromiseResult,
};

pub type AccountId = String;

pub type Timestamp = u64;

pub fn assert_self() {
let caller = env::predecessor_account_id();
let current = env::current_account_id();

assert_eq!(caller, current, "Only this contract may call itself");
}

pub fn assert_single_promise_success(){
assert_eq!(
env::promise_results_count(),
1,
"Expected exactly one promise result",
);

match env::promise_result(0) {
PromiseResult::Successful(_) => return,
_ => panic!("Expected PromiseStatus to be successful"),
};
}
  • The “models.rs” file contains some important structs (mainly the ‘Booking’ struct and its implementation block) to import into our main “lib.rs” file.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
#[allow(unused_imports)]
use near_sdk::{env, near_bindgen};
use near_sdk::serde::{Deserialize, Serialize};

use crate::utils::{
AccountId,
Timestamp
};

#[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[serde(crate = "near_sdk::serde")]
#[derive(Debug)]

pub struct Booking {
id: i32,
pub creator: AccountId,
created_at: Timestamp,
name: String,
nbr_days: u128,
total_price: u128,
description: String,
pub password: String,
}


impl Booking {
pub fn new(
id:i32,
name: String,
nbr_days:u128,
total_price:u128,
description: String
) -> Self {
Booking {
id,
creator: env::signer_account_id().to_string(),
created_at: env::block_timestamp(),
name,
nbr_days,
total_price,
description,
password: "tba".to_string(),
}
}
}
  • The “lib.rs” file is the actual smart contract. To briefly sum up what it’s doing, it allows users to list bookings with a name, a number of days, and a description. These bookings can be created by anyone signed into the app using their Near wallet and will generate a random password once the payment is received so that the user will be able to open the lock for the number of days it has paid for.
// partage-lock is a blockchain-controlled smart lock.
// it aims to provide utility owners with a device bridging digital and physical worlds for them to control users' access to vaulted utilities.
// a user will send a payment to a smart contract, which will generate a random password which will be sent secretly on the user email.
// the user will type that password on the keypad of the lock to access the locked utility.
// author: Julien Carbonnell @JCarbonnell for Partage @partage.btc
// all rights goes to CivicTech OÜ.

mod models;
mod utils;

use crate::{
utils::{
AccountId,
},
models::{
Booking
}
};

use std::convert::TryInto;

use near_sdk::{borsh::{self, BorshDeserialize, BorshSerialize}};
#[allow(unused_imports)]
use near_sdk::{env, PromiseIndex, Promise, near_bindgen};

near_sdk::setup_alloc!();


#[near_bindgen]
#[derive(Clone, Default, BorshDeserialize, BorshSerialize)]

pub struct Contract {
owner: AccountId,
bookings: Vec<Booking>,
}

#[near_bindgen]
impl Contract{
#[init]
// creating a new contract
pub fn new(
owner: AccountId,
) -> Self{
// creating a new booking vector within the contract struct
let bookings: Vec<Booking> = Vec::new();
Contract{
owner,
bookings
}
}

pub fn add_booking(
&mut self,
name: String,
nbr_days: u128,
total_price: u128,
description: String,
) {
// initiate a booking id
let id = self.bookings.len() as i32;
// transfer payment to the owner of the contract
let current = env::current_account_id();
Promise::new(current).transfer(total_price);
// create new booking on-chain with frontend data
self.bookings.push(Booking::new(
id,
name,
nbr_days,
total_price,
description,
));
self.add_password(id.try_into().unwrap());
env::log("Added a new booking!".as_bytes());
}

pub fn list_bookings(&self) -> Vec<Booking> {
let bookings = &self.bookings;
return bookings.to_vec();
}

pub fn booking_count(&mut self) -> usize {
return self.bookings.len();
}

fn add_password(&mut self, id:usize){
// get the booking
let booking: &mut Booking = self.bookings.get_mut(id).unwrap();
// replace the password in the booking
booking.password = "hashed".to_string();
env::log("Password added successfully to the booking!".as_bytes());
}

pub fn get_password(&mut self, id:usize) -> String {
let booking: &mut Booking = self.bookings.get_mut(id).unwrap();
return booking.password.to_string();

}
}

#[cfg(test)]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, AccountId};
fn get_context(predecessor: AccountId) -> VMContextBuilder {
let mut builder = VMContextBuilder::new();
builder.predecessor_account_id(predecessor);
builder
}

#[test]
fn test_add_booking() {
let alice = AccountId::new_unchecked("alice.testnet".to_string());
// Set up the testing context and unit test environment
let context = get_context(alice.clone());
testing_env!(context.build());
let mut contract = Contract::new(alice.to_string());
contract.add_booking("alice".to_string(), 6, 60, "I am booking the flat from March 20th to 26th. Everybody OK with it?".to_string());
let result = contract.booking_count();
assert_eq!(result, 1);
}
}

Frontend:

A demo of the Partage Lock is hosted at https://lock.hellopartage.xyz. It is composed of a landing page presenting the project: a demo video, a slide deck, and some key arguments explaining why we started developing this project. Once the user connects its Near wallet, the front end changes to a booking calendar connected to the smart contract deployed on Near.

2. Next Steps

So far the blockchain handles one reservation at a time. Every time one buyer buys access, the buyer’s payment will generate a new random password which will open the lock. To go further in the use case development, we would need to create some sort of calendar app on the blockchain, which will record a series of passwords working at different calendar dates. The bookings will need a new variable which will be a starting date, and we will need to find a way to avoid overlaps in dates between bookings when multi-users aren’t possible.

We also will look into a solution to increase the security of the system, to avoid malicious users being able to retrieve the code by its public hash.

Something else: we are building this app for a single smart lock. We will need to find a way to host more smart locks, make bookings in different locations in the world, and propose to our users a map and items that they can access. We will maybe change the smart contract architecture to handle more locks through some lockId, or keep deploying a single smart contract for each lock. There are pros and cons is such practice and we will need to dig further as we grow.

Resources:

--

--

Julien Carbonnell
Partage

CEO @partage // Urban Developer, Machine Learning, Blockchain Utility