Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize everything and expand readme #22

Merged
merged 3 commits into from
Jan 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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