Skip to content
Open
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
102 changes: 70 additions & 32 deletions src/shared/Angor.Shared/Protocol/Scripts/ProjectScriptsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@ public Script BuildInvestorInfoScript(ProjectInfo projectInfo, FundingParameters
var ops = new List<Op>
{
OpcodeType.OP_RETURN,
Op.GetPushOp(new PubKey(parameters.InvestorKey).ToBytes())
};

// V3+: prepend a 1-byte protocol version marker for unambiguous parsing
if (projectInfo.Version >= 3)
{
ops.Add(Op.GetPushOp(new byte[] { (byte)projectInfo.Version }));
}

ops.Add(Op.GetPushOp(new PubKey(parameters.InvestorKey).ToBytes()));

// For Fund/Subscribe projects, add dynamic stage info
if (projectInfo.ProjectType == ProjectType.Fund || projectInfo.ProjectType == ProjectType.Subscribe)
{
Expand All @@ -61,10 +68,17 @@ public Script BuildSeederInfoScript(ProjectInfo projectInfo, FundingParameters p
var ops = new List<Op>
{
OpcodeType.OP_RETURN,
Op.GetPushOp(new PubKey(parameters.InvestorKey).ToBytes()),
Op.GetPushOp((parameters.HashOfSecret ?? new uint256(0)).ToBytes())
};

// V3+: prepend a 1-byte protocol version marker for unambiguous parsing
if (projectInfo.Version >= 3)
{
ops.Add(Op.GetPushOp(new byte[] { (byte)projectInfo.Version }));
}

ops.Add(Op.GetPushOp(new PubKey(parameters.InvestorKey).ToBytes()));
ops.Add(Op.GetPushOp((parameters.HashOfSecret ?? new uint256(0)).ToBytes()));

// For Fund/Subscribe projects, add dynamic stage info
if (projectInfo.ProjectType == ProjectType.Fund || projectInfo.ProjectType == ProjectType.Subscribe)
{
Expand All @@ -91,57 +105,72 @@ public Script BuildSeederInfoScript(ProjectInfo projectInfo, FundingParameters p
if (ops.Count < 2 || ops[1].PushData == null)
throw new Exception($"Unexpected OP_RETURN format: expected at least 2 operations with push data, got {ops.Count}");

// Detect V3+ format: first push after OP_RETURN is a 1-byte version marker
int dataStartIndex = 1; // default: data starts at ops[1]
if (ops[1].PushData.Length == 1 && ops[1].PushData[0] >= 3)
{
// V3+ protocol: skip the version byte
dataStartIndex = 2;
if (ops.Count < 3 || ops[2].PushData == null)
throw new Exception($"V3 OP_RETURN: expected investor key after version byte, got {ops.Count - 1} data pushes");
}

// Validate investor key is a valid compressed public key (33 bytes)
if (ops[1].PushData.Length != 33)
throw new Exception($"Invalid investor key length in OP_RETURN: expected 33 bytes (compressed pubkey), got {ops[1].PushData.Length}");
if (ops[dataStartIndex].PushData.Length != 33)
throw new Exception($"Invalid investor key length in OP_RETURN: expected 33 bytes (compressed pubkey), got {ops[dataStartIndex].PushData.Length}");

if (ops.Count == 2)
var remainingOps = ops.Count - dataStartIndex;

if (remainingOps == 1)
{
// Invest project: investor key only
return (new PubKey(ops[1].PushData).ToHex(), null);
return (new PubKey(ops[dataStartIndex].PushData).ToHex(), null);
}

if (ops.Count == 3)
if (remainingOps == 2)
{
// Could be:
// 1. Invest project with seeder: investor key + secret hash (32 bytes)
// 2. Fund/Subscribe project: investor key + dynamic info (4 bytes)

if (ops[2].PushData == null)
throw new Exception("Unexpected OP_RETURN format: third operation has no push data");
var nextIdx = dataStartIndex + 1;
if (ops[nextIdx].PushData == null)
throw new Exception("Unexpected OP_RETURN format: push data is null after investor key");

if (ops[2].PushData.Length == 32)
if (ops[nextIdx].PushData.Length == 32)
{
// Seeder with secret hash
PubKey pubKey = new PubKey(ops[1].PushData);
uint256 secretHash = new uint256(ops[2].PushData);
PubKey pubKey = new PubKey(ops[dataStartIndex].PushData);
uint256 secretHash = new uint256(ops[nextIdx].PushData);
return (pubKey.ToHex(), secretHash);
}
else if (ops[2].PushData.Length == 4)
else if (ops[nextIdx].PushData.Length == 4)
{
// Dynamic stage info (no secret hash)
return (new PubKey(ops[1].PushData).ToHex(), null);
return (new PubKey(ops[dataStartIndex].PushData).ToHex(), null);
}
else
{
throw new Exception($"Unexpected OP_RETURN format: third push data has unrecognized length {ops[2].PushData.Length} (expected 32 for secret hash or 4 for dynamic info)");
throw new Exception($"Unexpected OP_RETURN format: push data has unrecognized length {ops[nextIdx].PushData.Length} (expected 32 for secret hash or 4 for dynamic info)");
}
}

if (ops.Count == 4)
if (remainingOps == 3)
{
// Fund/Subscribe seeder: investor key + secret hash + dynamic info
if (ops[2].PushData?.Length != 32)
throw new Exception($"Unexpected OP_RETURN format: expected 32-byte secret hash at position 2, got {ops[2].PushData?.Length}");
if (ops[3].PushData?.Length != 4)
throw new Exception($"Unexpected OP_RETURN format: expected 4-byte dynamic info at position 3, got {ops[3].PushData?.Length}");

PubKey pubKey = new PubKey(ops[1].PushData);
uint256 secretHash = new uint256(ops[2].PushData);
var hashIdx = dataStartIndex + 1;
var dynIdx = dataStartIndex + 2;
if (ops[hashIdx].PushData?.Length != 32)
throw new Exception($"Unexpected OP_RETURN format: expected 32-byte secret hash at position {hashIdx}, got {ops[hashIdx].PushData?.Length}");
if (ops[dynIdx].PushData?.Length != 4)
throw new Exception($"Unexpected OP_RETURN format: expected 4-byte dynamic info at position {dynIdx}, got {ops[dynIdx].PushData?.Length}");

PubKey pubKey = new PubKey(ops[dataStartIndex].PushData);
uint256 secretHash = new uint256(ops[hashIdx].PushData);
return (pubKey.ToHex(), secretHash);
}

throw new Exception($"Unexpected OP_RETURN format with {ops.Count} operations");
throw new Exception($"Unexpected OP_RETURN format with {ops.Count} operations (data starts at index {dataStartIndex})");
}

public DynamicStageInfo? GetDynamicStageInfoFromOpReturnScript(Script script)
Expand All @@ -153,17 +182,26 @@ public Script BuildSeederInfoScript(ProjectInfo projectInfo, FundingParameters p

var ops = script.ToOps();

// Check for dynamic stage info in different positions
if (ops.Count == 3 && ops[2].PushData?.Length == 4)
// Detect V3+ format: first push after OP_RETURN is a 1-byte version marker
int dataStartIndex = 1;
if (ops.Count >= 2 && ops[1].PushData?.Length == 1 && ops[1].PushData[0] >= 3)
{
dataStartIndex = 2; // skip version byte
}

var remainingOps = ops.Count - dataStartIndex;

// Check for dynamic stage info: last push is 4 bytes
if (remainingOps == 2 && ops[dataStartIndex + 1].PushData?.Length == 4)
{
// Investor with dynamic info: [OP_RETURN] [investor key] [dynamic info]
return DynamicStageInfo.FromBytes(ops[2].PushData);
// Investor with dynamic info: [investor key] [dynamic info]
return DynamicStageInfo.FromBytes(ops[dataStartIndex + 1].PushData);
}

if (ops.Count == 4 && ops[3].PushData?.Length == 4)
if (remainingOps == 3 && ops[dataStartIndex + 2].PushData?.Length == 4)
{
// Seeder with dynamic info: [OP_RETURN] [investor key] [secret hash] [dynamic info]
return DynamicStageInfo.FromBytes(ops[3].PushData);
// Seeder with dynamic info: [investor key] [secret hash] [dynamic info]
return DynamicStageInfo.FromBytes(ops[dataStartIndex + 2].PushData);
}

return null;
Expand Down
Loading