-
Notifications
You must be signed in to change notification settings - Fork 15
feat: shielded outputs integration #1059
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
base: master
Are you sure you want to change the base?
Changes from all commits
d8d2ad5
ee3f691
b1e1324
9315ebf
62612cc
aac3e62
ba29cac
89d8c52
0ee10f0
c453e43
1427322
30f4078
827fa09
be1cd7f
c01f7c1
af56e58
2368024
1e04651
f00b481
7cf5e49
a40326f
7e72bf1
b20af0c
1484f59
c406dba
d8011f6
0b89b44
5a34315
bdab561
7ba7abb
e32f589
78a8042
fd83b54
6782b7f
1efc856
ca42157
693f956
658692a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| /** | ||
| * Copyright (c) Hathor Labs and its affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| import ShieldedOutputsHeader from '../../src/headers/shielded_outputs'; | ||
| import ShieldedOutput from '../../src/models/shielded_output'; | ||
| import { ShieldedOutputMode } from '../../src/shielded/types'; | ||
| import Network from '../../src/models/network'; | ||
|
|
||
| function makeAmountShieldedOutput( | ||
| overrides: Partial<{ | ||
| commitment: Buffer; | ||
| rangeProof: Buffer; | ||
| tokenData: number; | ||
| script: Buffer; | ||
| ephemeralPubkey: Buffer; | ||
| }> = {} | ||
| ): ShieldedOutput { | ||
| return new ShieldedOutput( | ||
| ShieldedOutputMode.AMOUNT_SHIELDED, | ||
| overrides.commitment ?? Buffer.alloc(33, 0x01), | ||
| overrides.rangeProof ?? Buffer.from([0x02, 0x03, 0x04]), | ||
| overrides.tokenData ?? 0, | ||
| overrides.script ?? Buffer.from([0x76, 0xa9, 0x14]), | ||
| overrides.ephemeralPubkey ?? Buffer.alloc(33, 0x05) | ||
| ); | ||
| } | ||
|
|
||
| function makeFullShieldedOutput( | ||
| overrides: Partial<{ | ||
| commitment: Buffer; | ||
| rangeProof: Buffer; | ||
| script: Buffer; | ||
| ephemeralPubkey: Buffer; | ||
| assetCommitment: Buffer; | ||
| surjectionProof: Buffer; | ||
| }> = {} | ||
| ): ShieldedOutput { | ||
| return new ShieldedOutput( | ||
| ShieldedOutputMode.FULLY_SHIELDED, | ||
| overrides.commitment ?? Buffer.alloc(33, 0x11), | ||
| overrides.rangeProof ?? Buffer.from([0x22, 0x33]), | ||
| 0, | ||
| overrides.script ?? Buffer.from([0x76, 0xa9]), | ||
| overrides.ephemeralPubkey ?? Buffer.alloc(33, 0x44), | ||
| overrides.assetCommitment ?? Buffer.alloc(33, 0x55), | ||
| overrides.surjectionProof ?? Buffer.from([0x66, 0x77, 0x88]), | ||
| 0n | ||
| ); | ||
| } | ||
|
|
||
| describe('ShieldedOutputsHeader', () => { | ||
| const network = new Network('testnet'); | ||
|
|
||
| describe('serialize', () => { | ||
| it('should serialize header with AmountShielded outputs', () => { | ||
| const out1 = makeAmountShieldedOutput(); | ||
| const out2 = makeAmountShieldedOutput({ tokenData: 1 }); | ||
| const header = new ShieldedOutputsHeader([out1, out2]); | ||
|
|
||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const buf = Buffer.concat(parts); | ||
|
|
||
| // First byte is header ID (0x12) | ||
| expect(buf[0]).toBe(0x12); | ||
| // Second byte is number of outputs | ||
| expect(buf[1]).toBe(2); | ||
| }); | ||
|
|
||
| it('should serialize header with FullShielded outputs', () => { | ||
| const out = makeFullShieldedOutput(); | ||
| const header = new ShieldedOutputsHeader([out]); | ||
|
|
||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const buf = Buffer.concat(parts); | ||
|
|
||
| expect(buf[0]).toBe(0x12); | ||
| expect(buf[1]).toBe(1); | ||
| }); | ||
| }); | ||
|
|
||
| describe('serializeSighash', () => { | ||
| it('should produce different output from serialize (no proofs)', () => { | ||
| const out = makeAmountShieldedOutput(); | ||
| const header = new ShieldedOutputsHeader([out]); | ||
|
|
||
| const serParts: Buffer[] = []; | ||
| header.serialize(serParts); | ||
| const serialized = Buffer.concat(serParts); | ||
|
|
||
| const sighashParts: Buffer[] = []; | ||
| header.serializeSighash(sighashParts); | ||
| const sighash = Buffer.concat(sighashParts); | ||
|
|
||
| // Sighash should be shorter (no range_proof length prefix or data) | ||
| expect(sighash.length).toBeLessThan(serialized.length); | ||
| // Both should start with header ID and count | ||
| expect(sighash[0]).toBe(0x12); | ||
| expect(sighash[1]).toBe(1); | ||
| }); | ||
| }); | ||
|
|
||
| describe('deserialize', () => { | ||
| it('should round-trip AmountShielded outputs', () => { | ||
| const out1 = makeAmountShieldedOutput(); | ||
| const out2 = makeAmountShieldedOutput({ tokenData: 2, script: Buffer.from([0xab, 0xcd]) }); | ||
| const header = new ShieldedOutputsHeader([out1, out2]); | ||
|
|
||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const serialized = Buffer.concat(parts); | ||
|
|
||
| const [deserialized, remaining] = ShieldedOutputsHeader.deserialize(serialized, network); | ||
| const result = deserialized as ShieldedOutputsHeader; | ||
|
|
||
| expect(remaining.length).toBe(0); | ||
| expect(result.shieldedOutputs.length).toBe(2); | ||
|
|
||
| expect(result.shieldedOutputs[0].mode).toBe(ShieldedOutputMode.AMOUNT_SHIELDED); | ||
| expect(result.shieldedOutputs[0].commitment).toEqual(out1.commitment); | ||
| expect(result.shieldedOutputs[0].rangeProof).toEqual(out1.rangeProof); | ||
| expect(result.shieldedOutputs[0].tokenData).toBe(0); | ||
| expect(result.shieldedOutputs[0].script).toEqual(out1.script); | ||
| expect(result.shieldedOutputs[0].ephemeralPubkey).toEqual(out1.ephemeralPubkey); | ||
|
|
||
| expect(result.shieldedOutputs[1].tokenData).toBe(2); | ||
| expect(result.shieldedOutputs[1].script).toEqual(Buffer.from([0xab, 0xcd])); | ||
| }); | ||
|
|
||
| it('should round-trip FullShielded outputs', () => { | ||
| const out = makeFullShieldedOutput(); | ||
| const header = new ShieldedOutputsHeader([out]); | ||
|
|
||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const serialized = Buffer.concat(parts); | ||
|
|
||
| const [deserialized, remaining] = ShieldedOutputsHeader.deserialize(serialized, network); | ||
| const result = deserialized as ShieldedOutputsHeader; | ||
|
|
||
| expect(remaining.length).toBe(0); | ||
| expect(result.shieldedOutputs.length).toBe(1); | ||
|
|
||
| const d = result.shieldedOutputs[0]; | ||
| expect(d.mode).toBe(ShieldedOutputMode.FULLY_SHIELDED); | ||
| expect(d.commitment).toEqual(out.commitment); | ||
| expect(d.rangeProof).toEqual(out.rangeProof); | ||
| expect(d.script).toEqual(out.script); | ||
| expect(d.ephemeralPubkey).toEqual(out.ephemeralPubkey); | ||
| expect(d.assetCommitment).toEqual(out.assetCommitment); | ||
| expect(d.surjectionProof).toEqual(out.surjectionProof); | ||
| }); | ||
|
|
||
| it('should round-trip mixed AmountShielded and FullShielded outputs', () => { | ||
| const amountOut = makeAmountShieldedOutput({ tokenData: 1 }); | ||
| const fullOut = makeFullShieldedOutput(); | ||
| const header = new ShieldedOutputsHeader([amountOut, fullOut]); | ||
|
|
||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const serialized = Buffer.concat(parts); | ||
|
|
||
| const [deserialized, remaining] = ShieldedOutputsHeader.deserialize(serialized, network); | ||
| const result = deserialized as ShieldedOutputsHeader; | ||
|
|
||
| expect(remaining.length).toBe(0); | ||
| expect(result.shieldedOutputs.length).toBe(2); | ||
| expect(result.shieldedOutputs[0].mode).toBe(ShieldedOutputMode.AMOUNT_SHIELDED); | ||
| expect(result.shieldedOutputs[1].mode).toBe(ShieldedOutputMode.FULLY_SHIELDED); | ||
| }); | ||
|
|
||
| it('should preserve remaining buffer bytes', () => { | ||
| const out = makeAmountShieldedOutput(); | ||
| const header = new ShieldedOutputsHeader([out]); | ||
|
|
||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const trailingData = Buffer.from([0xfe, 0xed]); | ||
| const serialized = Buffer.concat([Buffer.concat(parts), trailingData]); | ||
|
|
||
| const [_, remaining] = ShieldedOutputsHeader.deserialize(serialized, network); | ||
| expect(remaining).toEqual(trailingData); | ||
| }); | ||
|
|
||
| it('should throw for invalid header ID', () => { | ||
| const buf = Buffer.from([0xff, 0x01]); | ||
| expect(() => ShieldedOutputsHeader.deserialize(buf, network)).toThrow('Invalid'); | ||
| }); | ||
|
|
||
| it('should re-serialize to identical bytes', () => { | ||
| const header = new ShieldedOutputsHeader([ | ||
| makeAmountShieldedOutput(), | ||
| makeFullShieldedOutput(), | ||
| ]); | ||
|
|
||
| const parts1: Buffer[] = []; | ||
| header.serialize(parts1); | ||
| const bytes1 = Buffer.concat(parts1); | ||
|
|
||
| const [deserialized] = ShieldedOutputsHeader.deserialize(bytes1, network); | ||
| const parts2: Buffer[] = []; | ||
| (deserialized as ShieldedOutputsHeader).serialize(parts2); | ||
| const bytes2 = Buffer.concat(parts2); | ||
|
|
||
| expect(bytes2).toEqual(bytes1); | ||
| }); | ||
| }); | ||
|
|
||
| describe('deserialization bounds checking', () => { | ||
| // Reuse suite-level `network` from line 56 | ||
| // Header ID (0x12) + numOutputs(1) + mode(1) = minimum 3 bytes before commitment | ||
| const headerId = 0x12; | ||
|
|
||
| it('should throw on truncated commitment', () => { | ||
| // header_id + num_outputs=1 + mode=1 + only 32 bytes (need 33) | ||
| const buf = Buffer.from([headerId, 0x01, 0x01, ...Array(32).fill(0)]); | ||
| expect(() => ShieldedOutputsHeader.deserialize(buf, network)).toThrow(/missing commitment/); | ||
| }); | ||
|
|
||
| it('should throw on truncated range proof', () => { | ||
| // header_id + num=1 + mode=1 + commitment(33) + rp_len=2 bytes saying 100 + only 10 bytes | ||
| const buf = Buffer.alloc(3 + 33 + 2 + 10); | ||
| buf[0] = headerId; | ||
| buf[1] = 1; // num outputs | ||
| buf[2] = 1; // mode AMOUNT_SHIELDED | ||
| buf.writeUInt16BE(100, 3 + 33); // range proof length = 100 | ||
| expect(() => ShieldedOutputsHeader.deserialize(buf, network)).toThrow( | ||
| /incomplete range proof/ | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw on unknown mode byte', () => { | ||
| // header_id + num=1 + mode=0x99 + enough bytes for commitment | ||
| const buf = Buffer.alloc(3 + 33 + 2 + 5 + 2 + 5 + 1 + 33); | ||
| buf[0] = headerId; | ||
| buf[1] = 1; | ||
| buf[2] = 0x99; // unknown mode | ||
| expect(() => ShieldedOutputsHeader.deserialize(buf, network)).toThrow( | ||
| /Unsupported shielded output mode: 153/ | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw on truncated ephemeral pubkey', () => { | ||
| // Build a valid AmountShielded up to the ephemeral pubkey, then truncate | ||
| const header = new ShieldedOutputsHeader([makeAmountShieldedOutput()]); | ||
| const parts: Buffer[] = []; | ||
| header.serialize(parts); | ||
| const full = Buffer.concat(parts); | ||
| // Trim the last 10 bytes (ephemeral pubkey is 33 bytes at the end) | ||
| const truncated = full.subarray(0, full.length - 10); | ||
| expect(() => ShieldedOutputsHeader.deserialize(truncated, network)).toThrow( | ||
| /missing ephemeral pubkey/ | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,16 @@ services: | |
| # All the following services are related to the core of the Private Network | ||
| # For more information on these configs, refer to: | ||
| # https://github.com/HathorNetwork/rfcs/blob/master/text/0033-private-network-guide.md | ||
| # Fullnode on 8080, tx-mining-service API on 8035, tx-mining-service DevMiner on 8034 | ||
|
|
||
| # Fullnode on 8080 , tx mining service on 8035, cpuminer stratum on 8034 | ||
|
|
||
| fullnode: | ||
| image: | ||
| ${HATHOR_LIB_INTEGRATION_TESTS_FULLNODE_IMAGE:-hathornetwork/hathor-core:sha-dc521be2} | ||
| ${HATHOR_LIB_INTEGRATION_TESTS_FULLNODE_IMAGE:-hathornetwork/hathor-core:experimental-shielded-outputs-alpha-v1} | ||
| command: [ | ||
| "run_node", | ||
| "--listen", "tcp:40404", | ||
| "--status", "8080", | ||
| "--test-mode-tx-weight", | ||
| "--test-mode-block-weight", | ||
| "--wallet-index", | ||
| "--allow-mining-without-peers", | ||
| "--unsafe-mode", "nano-testnet-bravo", | ||
|
|
@@ -44,7 +42,7 @@ services: | |
| tx-mining-service: | ||
| platform: linux/amd64 | ||
| image: | ||
| ${HATHOR_LIB_INTEGRATION_TESTS_TXMINING_IMAGE:-hathornetwork/tx-mining-service} | ||
| ${HATHOR_LIB_INTEGRATION_TESTS_TXMINING_IMAGE:-hathornetwork/tx-mining-service:shielded-outputs-v1} | ||
| depends_on: | ||
| fullnode: | ||
| condition: service_healthy | ||
|
|
@@ -54,11 +52,22 @@ services: | |
| command: [ | ||
| "http://fullnode:8080", | ||
| "--stratum-port=8034", | ||
| "--block-interval=1000", | ||
| "--api-port=8035", | ||
| "--dev-miner", | ||
| "--testnet", | ||
| "--address", "WTjhJXzQJETVx7BVXdyZmvk396DRRsubdw", # Miner rewards address (WALLET_CONSTANTS.miner in test-constants.ts) | ||
| "--testnet" | ||
| ] | ||
| networks: | ||
| - hathor-privnet | ||
|
|
||
| cpuminer: | ||
| image: hathornetwork/cpuminer | ||
| depends_on: | ||
| - tx-mining-service | ||
| command: [ | ||
| "-a", "sha256d", | ||
| "--coinbase-addr", "WTjhJXzQJETVx7BVXdyZmvk396DRRsubdw", # Refer to test-utils-integration.js, WALLET_CONSTANTS | ||
| "-o", "stratum+tcp://tx-mining-service:8034", | ||
| "--retry-pause", "5", # 5 seconds between retries | ||
| "-t", "1" # Number of threads used to mine | ||
| ] | ||
| networks: | ||
| - hathor-privnet | ||
|
|
@@ -228,8 +237,8 @@ services: | |
| AWS_SHARED_CREDENTIALS_FILE: ".aws/credentials" # Credentials for mocked AWS | ||
| AWS_CONFIG_FILE: ".aws/config" # Config for mocked AWS | ||
| ports: | ||
| - "3000:3000" | ||
| - "3001:3001" | ||
| - "3100:3000" | ||
| - "3101:3001" | ||
|
Comment on lines
+240
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for hardcoded references to old ports 3000/3001 in test files
echo "=== Searching for localhost:3000 or localhost:3001 references ==="
rg -n "localhost:300[01]|127.0.0.1:300[01]" --type ts --type js
echo ""
echo "=== Checking service-facade.helper.ts for port definitions ==="
fd -t f "service-facade.helper.ts" --exec cat {}Repository: HathorNetwork/hathor-wallet-lib Length of output: 8039 Update test helpers to use the new host ports 3100 and 3101. The docker-compose port mapping changed from
Tests will fail connecting to the services without these updates. 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a comment by myself about changes in this file. This will be addressed later There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| networks: | ||
| - hathor-privnet | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO we must make it work without cpuminer, using the dev miner