403 lines
13 KiB
TypeScript
403 lines
13 KiB
TypeScript
import * as anchor from "@coral-xyz/anchor";
|
|
import { Program } from "@coral-xyz/anchor";
|
|
import {
|
|
Ed25519Program,
|
|
PublicKey,
|
|
SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
Transaction,
|
|
} from "@solana/web3.js";
|
|
import { createHash } from "crypto";
|
|
import { expect } from "chai";
|
|
import { Shine } from "../target/types/shine";
|
|
|
|
const MAGIC = Buffer.from("SHiNE", "utf8");
|
|
const FORMAT_MAJOR = 1;
|
|
const FORMAT_MINOR = 0;
|
|
const ZERO_HASH = Buffer.alloc(32, 0);
|
|
const LAST_BLOCK_STATE_PREFIX = Buffer.from("SHiNE_LAST_BLOCK", "utf8");
|
|
const BLOCK_TYPE_ROOT_KEY = 1;
|
|
const BLOCK_TYPE_DEVICE_KEY = 2;
|
|
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
|
|
const BLOCK_TYPE_SERVER_PROFILE = 30;
|
|
const BLOCK_TYPE_ACCESS_SERVERS = 40;
|
|
const BLOCK_TYPE_TRUSTED_STATE = 50;
|
|
const BLOCK_VERSION_0 = 0;
|
|
const BLOCKCHAIN_TYPE_MAIN_USER = 1;
|
|
|
|
const LIMIT_STEP = 10_000n;
|
|
const START_BONUS_LIMIT = 100_000n;
|
|
const USERS_ECONOMY_CONFIG_SEED = "shine_users_economy_config";
|
|
const SHINE_PAYMENTS_PROGRAM_ID = new PublicKey(
|
|
"m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"
|
|
);
|
|
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = "shine_payments_inflow_vault";
|
|
|
|
type MutableFields = {
|
|
deviceKey: PublicKey;
|
|
blockchainPublicKey: PublicKey;
|
|
blockchainName: string;
|
|
usedBytes: bigint;
|
|
lastBlockNumber: number;
|
|
lastBlockHash: Buffer;
|
|
lastBlockSignature: Buffer;
|
|
arweaveTxId: string;
|
|
isServer: boolean;
|
|
serverKey: PublicKey;
|
|
serverAddress: string;
|
|
syncServers: string[];
|
|
accessServers: string[];
|
|
trustedCount: number;
|
|
};
|
|
|
|
type UnsignedRecord = {
|
|
createdAtMs: bigint;
|
|
updatedAtMs: bigint;
|
|
recordNumber: number;
|
|
prevRecordHash: Buffer;
|
|
login: string;
|
|
rootKey: PublicKey;
|
|
deviceKey: PublicKey;
|
|
blockchain: {
|
|
blockchainType: number;
|
|
blockchainName: string;
|
|
blockchainPublicKey: PublicKey;
|
|
paidLimitBytes: bigint;
|
|
usedBytes: bigint;
|
|
lastBlockNumber: number;
|
|
lastBlockHash: Buffer;
|
|
lastBlockSignature: Buffer;
|
|
arweaveTxId: string;
|
|
};
|
|
isServer: boolean;
|
|
serverKey: PublicKey;
|
|
serverAddress: string;
|
|
syncServers: string[];
|
|
accessServers: string[];
|
|
trustedCount: number;
|
|
};
|
|
|
|
function u32le(v: number): Buffer {
|
|
const b = Buffer.alloc(4);
|
|
b.writeUInt32LE(v, 0);
|
|
return b;
|
|
}
|
|
|
|
function u64le(v: bigint): Buffer {
|
|
const b = Buffer.alloc(8);
|
|
b.writeBigUInt64LE(v, 0);
|
|
return b;
|
|
}
|
|
|
|
function strBytes(value: string): Buffer {
|
|
const bytes = Buffer.from(value, "utf8");
|
|
return Buffer.concat([Buffer.from([bytes.length]), bytes]);
|
|
}
|
|
|
|
function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
|
|
const out: Buffer[] = [];
|
|
out.push(MAGIC);
|
|
out.push(Buffer.from([FORMAT_MAJOR]));
|
|
out.push(Buffer.from([FORMAT_MINOR]));
|
|
out.push(Buffer.alloc(2, 0)); // record_len placeholder
|
|
|
|
out.push(u64le(r.createdAtMs));
|
|
out.push(u64le(r.updatedAtMs));
|
|
out.push(u32le(r.recordNumber));
|
|
out.push(r.prevRecordHash);
|
|
out.push(strBytes(r.login));
|
|
|
|
out.push(Buffer.from([r.isServer ? 6 : 5]));
|
|
out.push(Buffer.from([BLOCK_TYPE_ROOT_KEY, BLOCK_VERSION_0]));
|
|
out.push(r.rootKey.toBuffer());
|
|
out.push(Buffer.from([BLOCK_TYPE_DEVICE_KEY, BLOCK_VERSION_0]));
|
|
out.push(r.deviceKey.toBuffer());
|
|
out.push(Buffer.from([BLOCK_TYPE_BLOCKCHAIN_REGISTRY, BLOCK_VERSION_0, 1]));
|
|
out.push(Buffer.from([r.blockchain.blockchainType]));
|
|
out.push(strBytes(r.blockchain.blockchainName));
|
|
out.push(r.blockchain.blockchainPublicKey.toBuffer());
|
|
out.push(u64le(r.blockchain.paidLimitBytes));
|
|
out.push(u64le(r.blockchain.usedBytes));
|
|
out.push(u32le(r.blockchain.lastBlockNumber));
|
|
out.push(r.blockchain.lastBlockHash);
|
|
out.push(r.blockchain.lastBlockSignature);
|
|
if (r.blockchain.arweaveTxId.length === 0) {
|
|
out.push(Buffer.from([0]));
|
|
} else {
|
|
out.push(Buffer.from([1]));
|
|
out.push(strBytes(r.blockchain.arweaveTxId));
|
|
}
|
|
|
|
if (r.isServer) {
|
|
out.push(Buffer.from([BLOCK_TYPE_SERVER_PROFILE, BLOCK_VERSION_0, 1]));
|
|
out.push(r.serverKey.toBuffer());
|
|
out.push(strBytes(r.serverAddress));
|
|
out.push(Buffer.from([r.syncServers.length]));
|
|
for (const s of r.syncServers) {
|
|
out.push(strBytes(s));
|
|
}
|
|
}
|
|
|
|
out.push(Buffer.from([BLOCK_TYPE_ACCESS_SERVERS, BLOCK_VERSION_0]));
|
|
out.push(Buffer.from([r.accessServers.length]));
|
|
for (const s of r.accessServers) {
|
|
out.push(strBytes(s));
|
|
}
|
|
out.push(Buffer.from([BLOCK_TYPE_TRUSTED_STATE, BLOCK_VERSION_0]));
|
|
out.push(Buffer.from([r.trustedCount]));
|
|
|
|
const unsigned = Buffer.concat(out);
|
|
const recordLen = unsigned.length + 64;
|
|
unsigned.writeUInt16LE(recordLen, 7);
|
|
return unsigned;
|
|
}
|
|
|
|
function sha256(buf: Buffer): Buffer {
|
|
return createHash("sha256").update(buf).digest();
|
|
}
|
|
|
|
function serializeLastBlockState(r: UnsignedRecord): Buffer {
|
|
return Buffer.concat([
|
|
LAST_BLOCK_STATE_PREFIX,
|
|
strBytes(r.login),
|
|
strBytes(r.blockchain.blockchainName),
|
|
u32le(r.blockchain.lastBlockNumber),
|
|
r.blockchain.lastBlockHash,
|
|
u64le(r.blockchain.usedBytes),
|
|
]);
|
|
}
|
|
|
|
function extractSigFromEdIx(ixData: Buffer): Buffer {
|
|
const signatureOffset = ixData.readUInt16LE(2);
|
|
return ixData.subarray(signatureOffset, signatureOffset + 64);
|
|
}
|
|
|
|
describe("shine_users e2e", () => {
|
|
anchor.setProvider(anchor.AnchorProvider.env());
|
|
const provider = anchor.getProvider() as anchor.AnchorProvider;
|
|
const program = anchor.workspace.shine as Program<Shine>;
|
|
|
|
it("registers user and updates balance/server data", async () => {
|
|
const login = `u${Date.now().toString().slice(-10)}`;
|
|
const [userPda] = PublicKey.findProgramAddressSync(
|
|
[Buffer.from("login="), Buffer.from(login, "utf8")],
|
|
program.programId
|
|
);
|
|
const [usersEconomyConfigPda] = PublicKey.findProgramAddressSync(
|
|
[Buffer.from(USERS_ECONOMY_CONFIG_SEED, "utf8")],
|
|
program.programId
|
|
);
|
|
const [inflowVaultPda] = PublicKey.findProgramAddressSync(
|
|
[Buffer.from(SHINE_PAYMENTS_INFLOW_VAULT_SEED, "utf8")],
|
|
SHINE_PAYMENTS_PROGRAM_ID
|
|
);
|
|
|
|
const economyAi = await provider.connection.getAccountInfo(
|
|
usersEconomyConfigPda
|
|
);
|
|
if (!economyAi) {
|
|
await program.methods
|
|
.initUsersEconomyConfig()
|
|
.accounts({
|
|
signer: provider.wallet.publicKey,
|
|
usersEconomyConfigPda,
|
|
})
|
|
.rpc();
|
|
}
|
|
|
|
const root = anchor.web3.Keypair.generate();
|
|
const blockchain = anchor.web3.Keypair.generate();
|
|
const deviceKey = anchor.web3.Keypair.generate().publicKey;
|
|
const serverKey1 = anchor.web3.Keypair.generate().publicKey;
|
|
const serverKey2 = anchor.web3.Keypair.generate().publicKey;
|
|
const blockchainName = `${login}-001`;
|
|
|
|
const createdAtMs = BigInt(Date.now());
|
|
const additionalLimitCreate = 20_000n;
|
|
expect(additionalLimitCreate % LIMIT_STEP).eq(0n);
|
|
|
|
const createRecord: UnsignedRecord = {
|
|
createdAtMs,
|
|
updatedAtMs: createdAtMs,
|
|
recordNumber: 0,
|
|
prevRecordHash: ZERO_HASH,
|
|
login,
|
|
rootKey: root.publicKey,
|
|
deviceKey,
|
|
blockchain: {
|
|
blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
|
|
blockchainName,
|
|
blockchainPublicKey: blockchain.publicKey,
|
|
paidLimitBytes: START_BONUS_LIMIT + additionalLimitCreate,
|
|
usedBytes: 0n,
|
|
lastBlockNumber: 0,
|
|
lastBlockHash: ZERO_HASH,
|
|
lastBlockSignature: Buffer.alloc(64, 0),
|
|
arweaveTxId: "",
|
|
},
|
|
isServer: true,
|
|
serverKey: serverKey1,
|
|
serverAddress: "https://srv-1.local",
|
|
syncServers: ["sync_srv_1", "sync_srv_2"],
|
|
accessServers: ["access_srv_1"],
|
|
trustedCount: 0,
|
|
};
|
|
const createLastBlockHash = sha256(serializeLastBlockState(createRecord));
|
|
const createLastBlockEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
|
privateKey: blockchain.secretKey,
|
|
message: createLastBlockHash,
|
|
});
|
|
createRecord.blockchain.lastBlockSignature = extractSigFromEdIx(
|
|
Buffer.from(createLastBlockEdIx.data)
|
|
);
|
|
|
|
const createUnsigned = serializeUnsignedRecord(createRecord);
|
|
const createHash = sha256(createUnsigned);
|
|
const createEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
|
privateKey: root.secretKey,
|
|
message: createHash,
|
|
});
|
|
const createSig = extractSigFromEdIx(Buffer.from(createEdIx.data));
|
|
|
|
const createIx = await program.methods
|
|
.createUserPda({
|
|
login,
|
|
rootKey: root.publicKey,
|
|
createdAtMs: new anchor.BN(createdAtMs.toString()),
|
|
additionalLimit: new anchor.BN(additionalLimitCreate.toString()),
|
|
fields: {
|
|
deviceKey,
|
|
blockchainPublicKey: blockchain.publicKey,
|
|
blockchainName,
|
|
usedBytes: new anchor.BN(
|
|
createRecord.blockchain.usedBytes.toString()
|
|
),
|
|
lastBlockNumber: createRecord.blockchain.lastBlockNumber,
|
|
lastBlockHash: createRecord.blockchain.lastBlockHash,
|
|
lastBlockSignature: createRecord.blockchain.lastBlockSignature,
|
|
arweaveTxId: "",
|
|
isServer: true,
|
|
serverKey: serverKey1,
|
|
serverAddress: "https://srv-1.local",
|
|
syncServers: ["sync_srv_1", "sync_srv_2"],
|
|
accessServers: ["access_srv_1"],
|
|
trustedCount: 0,
|
|
},
|
|
signature: createSig,
|
|
})
|
|
.accounts({
|
|
signer: provider.wallet.publicKey,
|
|
userPda,
|
|
inflowVault: inflowVaultPda,
|
|
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
usersEconomyConfigPda,
|
|
})
|
|
.instruction();
|
|
|
|
await provider.sendAndConfirm(
|
|
new Transaction().add(createEdIx, createLastBlockEdIx, createIx),
|
|
[]
|
|
);
|
|
|
|
const createAcc = await provider.connection.getAccountInfo(userPda);
|
|
expect(createAcc).not.eq(null);
|
|
expect(createAcc!.owner.toBase58()).eq(program.programId.toBase58());
|
|
|
|
const additionalLimitUpdate = 30_000n;
|
|
expect(additionalLimitUpdate % LIMIT_STEP).eq(0n);
|
|
|
|
const updateRecord: UnsignedRecord = {
|
|
createdAtMs,
|
|
updatedAtMs: createdAtMs + 1_000n,
|
|
recordNumber: 1,
|
|
prevRecordHash: sha256(createUnsigned),
|
|
login,
|
|
rootKey: root.publicKey,
|
|
deviceKey: anchor.web3.Keypair.generate().publicKey,
|
|
blockchain: {
|
|
blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
|
|
blockchainName,
|
|
blockchainPublicKey: blockchain.publicKey,
|
|
paidLimitBytes:
|
|
START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
|
|
usedBytes: 512n,
|
|
lastBlockNumber: 1,
|
|
lastBlockHash: sha256(Buffer.from("first-shine-block")),
|
|
lastBlockSignature: Buffer.alloc(64, 0),
|
|
arweaveTxId: "",
|
|
},
|
|
isServer: true,
|
|
serverKey: serverKey2,
|
|
serverAddress: "https://srv-2.local",
|
|
syncServers: ["sync_srv_3"],
|
|
accessServers: ["access_srv_2", "access_srv_3"],
|
|
trustedCount: 0,
|
|
};
|
|
const updateLastBlockHash = sha256(serializeLastBlockState(updateRecord));
|
|
const updateLastBlockEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
|
privateKey: blockchain.secretKey,
|
|
message: updateLastBlockHash,
|
|
});
|
|
updateRecord.blockchain.lastBlockSignature = extractSigFromEdIx(
|
|
Buffer.from(updateLastBlockEdIx.data)
|
|
);
|
|
|
|
const updateUnsigned = serializeUnsignedRecord(updateRecord);
|
|
const updateHash = sha256(updateUnsigned);
|
|
const updateEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
|
privateKey: root.secretKey,
|
|
message: updateHash,
|
|
});
|
|
const updateSig = extractSigFromEdIx(Buffer.from(updateEdIx.data));
|
|
|
|
const updateIx = await program.methods
|
|
.updateUserPda({
|
|
login,
|
|
rootKey: root.publicKey,
|
|
createdAtMs: new anchor.BN(createdAtMs.toString()),
|
|
updatedAtMs: new anchor.BN((createdAtMs + 1_000n).toString()),
|
|
version: 1,
|
|
prevHash: sha256(createUnsigned),
|
|
additionalLimit: new anchor.BN(additionalLimitUpdate.toString()),
|
|
fields: {
|
|
deviceKey: updateRecord.deviceKey,
|
|
blockchainPublicKey: updateRecord.blockchain.blockchainPublicKey,
|
|
blockchainName,
|
|
usedBytes: new anchor.BN(
|
|
updateRecord.blockchain.usedBytes.toString()
|
|
),
|
|
lastBlockNumber: updateRecord.blockchain.lastBlockNumber,
|
|
lastBlockHash: updateRecord.blockchain.lastBlockHash,
|
|
lastBlockSignature: updateRecord.blockchain.lastBlockSignature,
|
|
arweaveTxId: "",
|
|
isServer: true,
|
|
serverKey: serverKey2,
|
|
serverAddress: "https://srv-2.local",
|
|
syncServers: ["sync_srv_3"],
|
|
accessServers: ["access_srv_2", "access_srv_3"],
|
|
trustedCount: 0,
|
|
},
|
|
signature: updateSig,
|
|
})
|
|
.accounts({
|
|
signer: provider.wallet.publicKey,
|
|
userPda,
|
|
inflowVault: inflowVaultPda,
|
|
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
usersEconomyConfigPda,
|
|
})
|
|
.instruction();
|
|
|
|
await provider.sendAndConfirm(
|
|
new Transaction().add(updateEdIx, updateLastBlockEdIx, updateIx),
|
|
[]
|
|
);
|
|
|
|
const updatedAcc = await provider.connection.getAccountInfo(userPda);
|
|
expect(updatedAcc).not.eq(null);
|
|
const data = updatedAcc!.data;
|
|
expect(data.subarray(0, 5).toString("utf8")).eq("SHiNE");
|
|
expect(data[5]).eq(FORMAT_MAJOR);
|
|
expect(data[6]).eq(FORMAT_MINOR);
|
|
});
|
|
});
|