Skip to content

Commit

Permalink
Add readme and refactor code (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Jan 9, 2022
1 parent 010f9c0 commit 9e38296
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 212 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ description = "Satoshi tracker"
version = "0.0.0"
license = "CC0-1.0"
edition = "2021"
autotests = false

[dependencies]
bitcoin = "0.27.1"
executable-path = "1.0.0"
redb = "0.0.3"
structopt = "0.3.25"
tempfile = "3.2.0"

[[test]]
name = "integration"
path = "tests/lib.rs"
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,64 @@
# Sat Tracker

A scheme for numbering satoshis and and tracking them across transactions.
A scheme for assigning serial numbers to satoshis upon creation and tracking
them across subsequent transactions.

Satoshi serial numbers can be used as an addressing scheme for NFTs.


## Numbering

Satoshis are numbered in the order in which they are mined.

Satoshi numbers only depend on how many satoshis could have been created in
previous blocks, not how many were *actually* created.

In particular, this means that block 124724, which underpaid the block subsidy
by one, does not reduce the serial numbers of satoshis in subsequent blocks by
one.

The `range` command gives the half-open range of satoshis mined in the block at
a given height:

```
$ sat-tracker range 0
0 50000000000
```

See [src/range.rs](src/range.rs) for the numbering algorithm.


## Transfer

Satoshis input to a transaction are transferred to the transaction outputs
according to the order and value of the inputs and outputs. Satoshis paid as
fees are assigned in the same fashion to the outputs of the coinbase
transaction.

```rust
fn transfer(transaction: Transaction) {
let mut numbers = Vec::new();

for input in transaction.inputs {
for number in input.numbers {
numbers.push(number);
}
}

for output in transaction.outputs {
let rest = numbers.split_off(output.value);
output.numbers = numbers;
numbers = rest;
}

coinbase.input.numbers.extend_from_slice(&numbers);
}
```

The `find` command, unfinished, gives the current outpoint containing a given
satoshi as of a given height:

```
$ sat-tracker find --blocksdir ~/.bicoin/blocks 0 0
4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0
```
147 changes: 9 additions & 138 deletions src/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,156 +1,27 @@
use super::*;
use std::path::PathBuf;

const CHILDREN: &str = "children";
const HEIGHTS: &str = "heights";
const BLOCK_OFFSETS: &str = "block_offsets";
const HEIGHTS_TO_HASHES: &str = "height_to_hashes";

#[derive(StructOpt)]
pub enum Arguments {
FindSatoshi {
Find {
#[structopt(long)]
blocksdir: PathBuf,
n: u64,
at_height: u64,
height: u64,
},
Range {
height: u64,
},
}

impl Arguments {
pub fn run(self) -> Result<()> {
match self {
Self::FindSatoshi {
Self::Find {
blocksdir,
n,
at_height,
} => {
let tempdir = tempfile::tempdir()?;
let blockfile = blocksdir.join("blk00000.dat");

let db = unsafe {
Database::open(tempdir.path().join("bitcoin.redb"), 4096 * 1024 * 1024 * 10).unwrap()
};

{
let tx = db.begin_write()?;

let mut children: MultimapTable<[u8], [u8]> = tx.open_multimap_table(CHILDREN)?;

let mut block_offsets: Table<[u8], u64> = tx.open_table(BLOCK_OFFSETS)?;

let blocks = fs::read(&blockfile)?;

let mut i = 0;

let mut count = 0;

loop {
if i == blocks.len() {
break;
}

let offset = i;

assert_eq!(&blocks[i..i + 4], &[0xf9, 0xbe, 0xb4, 0xd9]);
i += 4;

let len = u32::from_le_bytes(blocks[i..i + 4].try_into()?) as usize;
i += 4;

let bytes = &blocks[i..i + len];
i += len;

let block = Block::consensus_decode(bytes)?;

children.insert(&block.header.prev_blockhash, &block.block_hash())?;

block_offsets.insert(&block.block_hash(), &(offset as u64))?;

count += 1;
}

eprintln!("Inserted {} blocks…", count);

tx.commit()?;
}

{
let write = db.begin_write()?;

let mut heights: Table<[u8], u64> = write.open_table(HEIGHTS)?;
let mut heights_to_hashes: Table<u64, [u8]> = write.open_table(HEIGHTS_TO_HASHES)?;

heights.insert(genesis_block(Network::Bitcoin).block_hash().deref(), &0)?;
heights_to_hashes.insert(&0, genesis_block(Network::Bitcoin).block_hash().deref())?;

let read = db.begin_read()?;

let children: ReadOnlyMultimapTable<[u8], [u8]> = read.open_multimap_table(CHILDREN)?;

let mut queue = vec![(
genesis_block(Network::Bitcoin)
.block_hash()
.deref()
.to_vec(),
0,
)];

while let Some((block, height)) = queue.pop() {
heights.insert(block.as_ref(), &height)?;
heights_to_hashes.insert(&height, block.as_ref())?;

let mut iter = children.get(&block)?;

while let Some(child) = iter.next() {
queue.push((child.to_vec(), height + 1));
}
}

write.commit()?;
}

let height = n / (50 * 100_000_000);
assert!(height < 100);
assert!(at_height == height);

let tx = db.begin_read()?;

let heights_to_hashes: ReadOnlyTable<u64, [u8]> = tx.open_table(HEIGHTS_TO_HASHES)?;
let guard = heights_to_hashes.get(&height)?.unwrap();
let hash = guard.to_value();

let offsets: ReadOnlyTable<[u8], u64> = tx.open_table(BLOCK_OFFSETS)?;
let mut i = offsets.get(hash)?.unwrap().to_value() as usize;

if i == 1 {
i = 0;
}

let blocks = fs::read(&blockfile)?;

assert_eq!(&blocks[i..i + 4], &[0xf9, 0xbe, 0xb4, 0xd9]);
i += 4;

let len = u32::from_le_bytes(blocks[i..i + 4].try_into()?) as usize;
i += 4;

let bytes = &blocks[i..i + len];

let block = Block::consensus_decode(bytes)?;

let position = n % (50 * 100_000_000);

let mut n = 0;
for (i, output) in block.txdata[0].output.iter().enumerate() {
if n + output.value >= position {
println!("{}:{}", block.txdata[0].txid(), i);
break;
}
n += output.value;
}

Ok(())
}
height,
} => crate::find::run(&blocksdir, n, height),
Self::Range { height } => crate::range::run(height),
}
}
}
135 changes: 135 additions & 0 deletions src/find.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use super::*;

const CHILDREN: &str = "children";
const HEIGHTS: &str = "heights";
const BLOCK_OFFSETS: &str = "block_offsets";
const HEIGHTS_TO_HASHES: &str = "height_to_hashes";

pub(crate) fn run(blocksdir: &Path, n: u64, at_height: u64) -> Result<()> {
let tempdir = tempfile::tempdir()?;
let blockfile = blocksdir.join("blk00000.dat");

let db = unsafe {
Database::open(tempdir.path().join("bitcoin.redb"), 4096 * 1024 * 1024 * 10).unwrap()
};

{
let tx = db.begin_write()?;

let mut children: MultimapTable<[u8], [u8]> = tx.open_multimap_table(CHILDREN)?;

let mut block_offsets: Table<[u8], u64> = tx.open_table(BLOCK_OFFSETS)?;

let blocks = fs::read(&blockfile)?;

let mut i = 0;

let mut count = 0;

loop {
if i == blocks.len() {
break;
}

let offset = i;

assert_eq!(&blocks[i..i + 4], &[0xf9, 0xbe, 0xb4, 0xd9]);
i += 4;

let len = u32::from_le_bytes(blocks[i..i + 4].try_into()?) as usize;
i += 4;

let bytes = &blocks[i..i + len];
i += len;

let block = Block::consensus_decode(bytes)?;

children.insert(&block.header.prev_blockhash, &block.block_hash())?;

block_offsets.insert(&block.block_hash(), &(offset as u64))?;

count += 1;
}

eprintln!("Inserted {} blocks…", count);

tx.commit()?;
}

{
let write = db.begin_write()?;

let mut heights: Table<[u8], u64> = write.open_table(HEIGHTS)?;
let mut heights_to_hashes: Table<u64, [u8]> = write.open_table(HEIGHTS_TO_HASHES)?;

heights.insert(genesis_block(Network::Bitcoin).block_hash().deref(), &0)?;
heights_to_hashes.insert(&0, genesis_block(Network::Bitcoin).block_hash().deref())?;

let read = db.begin_read()?;

let children: ReadOnlyMultimapTable<[u8], [u8]> = read.open_multimap_table(CHILDREN)?;

let mut queue = vec![(
genesis_block(Network::Bitcoin)
.block_hash()
.deref()
.to_vec(),
0,
)];

while let Some((block, height)) = queue.pop() {
heights.insert(block.as_ref(), &height)?;
heights_to_hashes.insert(&height, block.as_ref())?;

let mut iter = children.get(&block)?;

while let Some(child) = iter.next() {
queue.push((child.to_vec(), height + 1));
}
}

write.commit()?;
}

let height = n / (50 * 100_000_000);
assert!(height < 100);
assert!(at_height == height);

let tx = db.begin_read()?;

let heights_to_hashes: ReadOnlyTable<u64, [u8]> = tx.open_table(HEIGHTS_TO_HASHES)?;
let guard = heights_to_hashes.get(&height)?.unwrap();
let hash = guard.to_value();

let offsets: ReadOnlyTable<[u8], u64> = tx.open_table(BLOCK_OFFSETS)?;
let mut i = offsets.get(hash)?.unwrap().to_value() as usize;

if i == 1 {
i = 0;
}

let blocks = fs::read(&blockfile)?;

assert_eq!(&blocks[i..i + 4], &[0xf9, 0xbe, 0xb4, 0xd9]);
i += 4;

let len = u32::from_le_bytes(blocks[i..i + 4].try_into()?) as usize;
i += 4;

let bytes = &blocks[i..i + len];

let block = Block::consensus_decode(bytes)?;

let position = n % (50 * 100_000_000);

let mut n = 0;
for (i, output) in block.txdata[0].output.iter().enumerate() {
if n + output.value >= position {
println!("{}:{}", block.txdata[0].txid(), i);
break;
}
n += output.value;
}

Ok(())
}
Loading

0 comments on commit 9e38296

Please sign in to comment.